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

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

こんにちは.高山です.
以前の記事で,動作追跡点の欠損値を線形補間で穴埋めする方法を紹介しました.
今回は,Pythonの行列演算用ライブラリNumpyを用いて線形補間を実装する方法を紹介します.
Numpyには線形補間を行う関数が用意されており,簡単に実装することができます.
線形補間は色々な場面で使える処理ですので,ご一読いただければ幸いです.
今回解説するスクリプトはGitHub上に公開しています


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

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

1. モジュールのインストールとロード

第1節ではモジュールのインストールとロードを行います.
第1節と第2節では入力データを確認するために,全身追跡で紹介した内容と同様の処理を行っています.
線形補間の具体的な処理については第4節から説明しますのでそちらから読み進めていただいても構いません.

1.1 MediaPipeのインストール

追跡点の描画のために,MediaPipeから追跡点接続関係の定義をインポートします.
初期状態のColab環境にはMediaPipeがインストールされていませんので,Colab環境にMediaPipeをインストールします.

Colab環境では,先頭に"!"付けるとその行はShellコマンドとみなされます.
下のコードでは,Pythonのパッケージ管理ツール pip を呼び出してMediaPipeのバージョン"0.10.0"をインストールしています.

!pip3 install mediapipe==0.10.0

1.2 利用モジュールのインポート

次のコードでは使用するモジュールをインポートしています.
この操作によって,各モジュールに実装されている機能を利用できるようになります.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Standard modules.
import gc
import sys
import time
from functools import partial
from pathlib import Path

# CV/ML.
import cv2

import numpy as np

# For drawing.
from mediapipe.python.solutions.face_mesh_connections import (
    FACEMESH_CONTOURS)
from mediapipe.python.solutions.hands_connections import (
    HAND_CONNECTIONS)
from mediapipe.python.solutions.pose_connections import (
    POSE_CONNECTIONS)

from IPython.display import HTML
from base64 import b64encode
import io
【コード解説】
- 標準モジュール
  - gc: ガベージコレクション用ライブラリ
    処理時間計測クラスの内部処理で用います.
  - sys: Pythonシステムを扱うライブラリ
    今回はバージョン情報を取得するために使用しています.
  - time: 時刻データを取り扱うためのライブラリ
    今回は処理時間を計測するために使用しています.
  - pathlib: オブジェクト指向のファイルパスクラス
    ファイルの作成・削除操作のために使用しています.
  - functools: 関数オブジェクトを操作するためのライブラリ
    処理時間計測クラスに渡す関数オブジェクト作成に使用しています.
- 画像処理・機械学習向けモジュール
  - cv: 画像処理ライブラリOpenCVのPython版
  - numpy: 行列演算ライブラリ
- 描画処理向けモジュール
  - FACEMESH_CONTOURS: MediaPipe向け顔追跡点の接続関係を定義したデータ
  - HAND_CONNECTIONS: MediaPipe向け手追跡点の接続関係を定義したデータ
  - POSE_CONNECTIONS: MediaPipe向け身体追跡点の接続関係を定義したデータ
  - HTML: 生データをロードしてHTML形式に変換する機能.今回は動画を描画するために使用
  - b64encode: データをbase64形式に変換する機能.
    base64はデータをアルファベット,数字,記号の64文字で表すデータ形式
  - io: データやファイルに対する入出力機能.標準モジュールだが今回は動画を読み込むために使用

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

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

2. テストデータのロードと確認

第2節では,追跡点確認用データとして追跡点生成元の動画ファイルと,補間前の追跡点データをダウンロードしています.

2.1 テストデータのダウンロード

今回使用した動画ファイルは,GitHub上に公開しています.

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

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

!ls
finger_far0.mp4  finger_far0_non_static.npy  sample_data

2.2 元動画を描画して確認

次のスクリプトはColab上で動画を描画する機能を実装しています.

1
2
3
4
5
6
7
8
def show_video(video_path, video_width=500, video_height=500):
    video = io.open(video_path, 'r+b').read()
    encoded = b64encode(video)
    decoded = encoded.decode("ascii")
    data = f"<video width={video_width} height={video_height} controls>" \
        + f'<source src="data:video/mp4;base64,{decoded}"' \
        + 'type="video/mp4" /></video>'
    return(HTML(data=data))
【コード解説】
- 引数
  - video_path: 入力動画のパス
  - video_width: 動画表示領域の幅
  - video_height: 動画表示領域の高さ
- 2行目: 動画ファイルをオープン
- 3-4行目: 生データを一旦base64形式に変換し,さらにASCII文字列に変換
- 5-7行目: HTMLのvideoタグを表す文字列を定義
- 8行目: dataをHTML形式に変換して返す

次のコードでダウンロードした動画をColab上に表示しています.
show_video() の返り値がHTMLオブジェクトのため(videoタグ),埋め込み動画が表示されます.

videopath_orig = Path("./finger_far0.mp4")
show_video(videopath_orig)

2.3 追跡点を描画して確認

追跡点の描画処理

次のコードは,描画ウィンドウに対して追跡点を描画しています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def draw_landmarks(draw, landmarks, connections,
                   pt_color=(0, 0, 0), line_color=(0, 0, 0),
                   pt_size=3, line_size=3,
                   do_remove_error=True):
    for i, line in enumerate(connections):
        p0 = landmarks[line[0], :2]
        p1 = landmarks[line[1], :2]
        if do_remove_error:
            if np.isnan(p0).any() or np.isnan(p1).any():
                continue
            if (p0==0).any() or (p1==0).any():
                continue
        p0 = (int(p0[0]), int(p0[1]))
        p1 = (int(p1[0]), int(p1[1]))
        cv2.line(draw, p0, p1, line_color, line_size)
        cv2.circle(draw, p0, pt_size, pt_color, -1)
        cv2.circle(draw, p1, pt_size, pt_color, -1)
    return draw
【コード解説】
- 引数
  - draw: 描画領域.背景は既に描画済みの想定です.
  - landmarks: 追跡点配列
  - connections: 追跡点間の接続関係
    接続関係は `[[0, 1], [1, 2], ...]` のように追跡点インデクスのペアの配列になっています.
  - pt_color: 追跡点の描画色 (BGR)形式
  - line_color: 追跡点間の描画色 (BGR)形式
  - pt_size: 追跡点の描画サイズ
  - line_size: 追跡点間の描画サイズ
  - do_remove_error: `True` の場合,追跡に失敗している点は無視する
- 5-7行目: 追跡点間の接続関係と,対応関係になっている2点を抽出
  追跡点の描画には $(x, y)$ 座標だけを用います.
- 8-12行目: 追跡に失敗した点をスキップ
  `Nan` や座標値が `0` になっている場合は描画をスキップします.
- 13-17行目: 追跡点の接続関係を直線で描画し,追跡点を円で描画しています.

追跡点描画のメイン処理

次のコードは,描画メイン処理を実装しています.

 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def drawing(videofile, trackfile, outvideo):
    trackdata = np.load(trackfile)
    if os.path.exists(videofile):
        capture = cv2.VideoCapture(videofile)
        capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
        width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
    else:
        capture = None
        width = 500
        height = 500

    xmin_orig = trackdata[:, :, :, 0].min()
    ymin_orig = trackdata[:, :, :, 1].min()
    xmax_orig = trackdata[:, :, :, 0].max()
    ymax_orig = trackdata[:, :, :, 1].max()
    if xmax_orig < 10 and ymax_orig < 10:
        trackdata[:, :, 0] *= width
        trackdata[:, :, 1] *= height
    xmin_proj = int(xmin_orig)
    ymin_proj = int(ymin_orig)
    xmax_proj = int(xmax_orig)
    ymax_proj = int(ymax_orig)

    # Add offset.
    offset_x = 0
    offset_y = 0
    if xmin_proj < 0:
        offset_x = -xmin_proj
        trackdata[:, :, :, 0] += offset_x
    if ymin_proj < 0:
        offset_y = -ymin_proj
        trackdata[:, :, :, 1] += offset_y

    ywin = int(max(ymax_proj + offset_y, height + offset_y))
    xwin = int(max(xmax_proj + offset_x, width + offset_x))

    print("Window size:", xwin, ywin)
    print("Offsets:", offset_x, offset_y)

    writer = cv2.VideoWriter(outvideo, cv2.VideoWriter_fourcc(*"mp4v"),
        30.0, (xwin, ywin))
    if writer.isOpened() is False:
        print("VideoWriter is failed to open.")
        if writer is not None:
            writer.release()
        raise ValueError(f"Can not open {videofile}")

    # `[P, T, J, C] -> [T, P, J, C]`
    trackdata = np.transpose(trackdata, [1, 0, 2, 3])
    for frame in trackdata:
        if capture is not None:
            status, image = capture.read()
            if status is False:
                break
        else:
            image = np.full([height, width], 255, dtype=np.uint8)

        # Draw.
        draw = np.full([int(ywin), int(xwin), 3], 64, dtype=np.uint8)
        draw[offset_y: offset_y+height, offset_x: offset_x+width, :] = image

        for instance in frame:
            body = instance[:33, :]
            lhand = instance[33:33+21, :]
            rhand = instance[33+21:33+21+21, :]
            face = instance[33+21+21:, :]
            draw_landmarks(draw, body, POSE_CONNECTIONS, [0, 255, 0], [0, 255, 0],
                           do_remove_error=True)
            draw_landmarks(draw, lhand, HAND_CONNECTIONS, [255, 0, 0], [255, 0, 0],
                           do_remove_error=True)
            draw_landmarks(draw, rhand, HAND_CONNECTIONS, [0, 0, 255], [0, 0, 255],
                           do_remove_error=True)
            draw_landmarks(draw, face, FACEMESH_CONTOURS, [255, 255, 0], [255, 255, 0],
                           do_remove_error=True)
        writer.write(draw)
    writer.release()
    if capture is not None:
        capture.release()
【コード解説】
- 引数:
  - videofile: 入力動画のパス.動画は背景画像を描画するために用います.
  - trackfile: 追跡データファイルのパス
  - outvideo: 結果動画の出力パス
- 2-11行目: 追跡点ファイルと動画ファイルをロード
  動画ファイルのオープンに成功した場合は,読み込み位置を先頭フレームに設定し,画像幅と画像高さを読み込みます.
- 13-23行目: 追跡点座標の最大値と最小値を算出
  この値はウィンドウサイズと描画位置の調整に用います.
  また,先の追跡処理でスケーリングをしていない場合は,19-21行目で再度スケーリングを行います.
- 25-39行目: ウィンドウサイズと描画位置の調整
  この処理によって追跡点座標が負の値や画面外の値になっている場合でも描画できるようになります.
- 41-47行目: 出力用の動画ファイルをオープン
  オープンに失敗した場合は例外を投げて中断します.
- 50-76行目: メインループ
    - 50行目: 時間次元に沿ってループ処理を行うために,追跡点配列の第1次元と第2次元を入れ替え
    - 52-61行目: 画像フレームを読み込み,背景を描画
      入力動画をオープンしていない場合は白背景を設定します.
    - 63行目: 人物毎に追跡点を描画
      全身追跡モードは複数名の追跡に未対応なためここでのループは不要なのですが,他の追跡処理と互換性をもたせるためにこのような処理になっています.
    - 64-75行目: 部位毎に追跡点を描画
      追跡点の接続関係は部位毎に定義されているため,一旦追跡点を部位毎に分割して,それぞれ異なる色で描画しています.
    - 76行目: 描画フレームを動画ファイルに書き出し
- 77-79行目: 動画の保存および終了処理

追跡点描画の実行

次のコードで追跡データを描画しています.
この処理は特に難しいことはなく,最初に引数を設定して描画関数に渡しているだけです.

1
2
3
4
trackfile = Path("./finger_far0_non_static.npy")
outvideo = Path("./finger_far0_track_orig.mp4")

drawing(videopath_orig, trackfile, outvideo)

描画結果を表示して確認をします.
OpenCVを使って作成した動画ファイルはColab環境上では上手く表示できなかったため,FFMPEGを使用して変換を行います.

!ffmpeg -i finger_far0_track_orig.mp4 -vcodec vp9 -y finger_far0_track_orig.webm

その後,次のコードで動画ファイルの描画を行います.

show_video(outvideo.stem + ".webm")

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. 線形補間の実装

第3節では,線形補間処理の実装方法を紹介します.

4.1 線形補間関数の説明

今回は,Numpyに実装されている線形補間関数 interp を用いて処理を行います.

interpには \(x, xp, fp\) の引数を与える必要があります.
(その他に3個のオプション引数があります)
3種類の引数はそれぞれ下記を示します.

  • \(x\): 補間対象の \(x\) 座標値
  • \(xp\): データ点の \(x\) 座標値.オプション引数で明示的に指定されない場合は,この値は単調増加である必要があります.
  • \(fp\): データ点の \(y\) 座標値.

例えば,データ点を \((xp, fp)=\{(0, 0), (2, 4), (4, 8)\}\) として \(x=\{0, 1, 2, 3, 4\}\) を与えた場合,interpは \(\{(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)\}\) を返します.

4.2 線形補間処理

interpの引数 \(x, xp, fp\) は1次元配列である必要があります.
そのため,追跡点の補間に用いるためにはループ処理で追跡点次元と特徴量次元を取り出し逐次的に処理を行います.

次のコードで上記で説明した内容を実装しています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def simple_interp(trackdata):
    tlength, num_joints, _ = trackdata.shape
    newtrack = np.zeros_like(trackdata)
    for i in range(num_joints):
        temp = trackdata[:, i, :]
        mask = temp[:, -1] != 0
        valid = mask.sum()
        if valid == tlength:
            newtrack[:, i] = temp
            continue
        xs = np.where(mask != 0)[0]
        ys = temp[xs, :]
        newys = np.zeros_like(temp)
        for j in range(temp.shape[-1]):
            newy = np.interp(np.arange(tlength), xs, ys[:, j])
            newys[:, j] = newy
        newtrack[:, i] = newys
    return newtrack
【コード解説】
- 引数
  - trackdata: `[T, J, C]` 形状の追跡点配列.欠損値はゼロ埋めされている必要があります.
    - T: 動画フレームインデクス
    - J: 追跡点インデクス
    - C: 特徴量インデクス.今回は $(x, y, z, c)$ の4次元特徴量を用いています.
- 2行目: 処理内部で用いるために,入力配列の時間長と追跡点数を取り出す
- 3行目: 補間後の配列格納用変数を初期化
- 4-17行目: 補間処理のループ
  - 5行目: $i$ 番目の追跡点配列を取り出す.取り出した後の配列は`[T, C]`形状になります.
  - 6行目: 末尾の特徴量 $(c)$ が $0$ "でない場合"に`True`,$0$ の場合に`False`となる2値マスク配列を生成.
    $c$ は追跡信頼度を示していますので,$c=0$ は追跡失敗を示します.
  - 7行目: 追跡成功フレーム数を算出
  - 8-10行目: 追跡成功フレーム数が動画フレーム数と同じ場合は補間の必要が無いので,そのまま配列を格納して次のループへ移行
  - 11-12行目: 追跡成功フレームの時間インデクス `xs` と 特徴量 `ys` を取り出す
  - 13行目: 補間後の配列格納用変数 (一時変数) を初期化
  - 14-17行目: 特徴次元に沿ってループを回し線形補間,その後配列に値を格納

4.3 補間処理の実行

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

追跡点データの確認

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

1
2
3
4
5
6
7
# Load track data.
trackdata = np.load(trackfile)
print(trackdata.shape)

# Remove person axis.
trackdata = trackdata.squeeze()
print(trackdata.shape)

print処理の結果を示します.

(1, 130, 553, 4)
(130, 553, 4)

入力データは \((P, T, J, C)=(1, 130, 553, 4)\) 形状になっています.

データに欠損値が含まれていることを確認してみます.
次のコードでは各追跡点毎に欠損値の有無を確認して,欠損値が含まれている場合は追跡成功フレーム数を表示しています.

1
2
3
4
5
6
7
8
# Print joints including missing frames.
tlength, num_joints, num_featurs = trackdata.shape
for i in range(num_joints):
    temp = trackdata[:, i, :]
    mask = temp[:, -1] != 0
    valid = mask.sum()
    if valid != tlength:
        print(i, valid)

print処理の結果を示します.

54 103
55 103
56 103
57 103
58 103
...
74 103

各行の1番目の数値は追跡点インデクスを示し,2番目の数値は追跡成功フレーム数を示しています.
54-74番目までの追跡点で,27フレーム追跡に失敗していることが分かります.
なお,54-74番目の追跡点は右手の追跡点を示します.

補間処理を適用

では,補間処理を適用してみます.
次のコードは先程実装したsimple_interptrackdataを入力して,補間後の追跡点newtrackを得ています.
その後補間結果の確認用に,第2節で実装した描画処理に合わせて形状を調整して追跡データを保存しています.

1
2
3
4
5
6
7
8
# Apply interpolation.
newtrack = simple_interp(trackdata)

# Add person axis.
newtrack = np.expand_dims(newtrack, 0)

outpath_track = trackfile.stem + "_interp.npy"
np.save(outpath_track, newtrack)

4.4 補間結果の確認

では,補間結果を実際に描画して確認してみます.
第2節の処理と同じく,次のコードで追跡データを描画して動画ファイルに保存します.

1
2
3
4
# Draw result.
outvideo = Path("./finger_far0_track_interp.mp4")

drawing(videopath_orig, outpath_track, outvideo)

第2節の処理と同じく,Colab上で表示できるように動画ファイルを変換します.

!ffmpeg -i finger_far0_track_interp.mp4 -vcodec vp9 -y finger_far0_track_interp.webm

その後,次のコードで動画ファイルの描画を行います.

1
show_video(outvideo.stem + ".webm")

4.5 処理時間の計測

最後に,次のコードで処理時間を計測します.

1
2
target_fn = partial(simple_interp, trackdata=trackdata)
pmeasure(target_fn)

最初に,partial()simple_interp() に入力を指定して関数オブジェクトにしています.
次に,第3節で実装した処理時間計測クラスに関数オブジェクトを渡して処理時間を計測しています.

print処理の結果を示します.

Overall summary: Max 16.6795ms, Min 6.78701ms, Mean +/- Std 7.66203ms +/- 1.29474ms
Top 10 summary: Max 6.95392ms, Min 6.78701ms, Mean +/- Std 6.8714ms +/- 59.721µs

(他のプロセスの影響が少ない場合は) 線形補間は平均して \(6.87\) ミリ秒程度かかることが分かります.
処理時間を高速化する方法は別記事で紹介しようと思います.
(\(6.87\) ミリ秒を遅いと見るかはアプリケーション次第ですが...(^^;))


今回はNumpyの線形補間関数を用いて追跡点欠損値を穴埋めするプログラムの解説を行いましたが,如何でしょうか?
機械学習などの大量のデータを扱う場合に,全てのデータが綺麗な状態であることは稀で,何らかの欠損値がある場合がよくあります.
線形補間は追跡点のように時間軸に沿って連続した特徴量を補間するための標準的な手法の一つです.

線形補間は単純ですが計算量が少なくて理解もしやすいので,欠損値の補間以外にも様々なアプリケーションで用いられています.
今回紹介した話が,補間処理などでお悩みの方に何か参考になれば幸いです.