TF.js 実践開発レシピ

TensorFlow.jsで実装する実践的な画像前処理と後処理:Pythonの画像ライブラリとの比較

Tags: TensorFlow.js, 画像処理, 前処理, 後処理, Python, JavaScript

はじめに

TensorFlow.jsを用いた画像認識AIの開発において、モデルの推論を実行する前に行う画像前処理や、推論結果を受け取った後に行う画像後処理は非常に重要なステップです。これらの処理は、モデルが期待する入力形式に画像を整えたり、モデルの出力を人間が理解できる形に変換したりするために不可欠です。

Pythonによる機械学習開発の経験があるエンジニアの方であれば、OpenCV、PIL (Pillow)、NumPy、あるいはTensorFlow/Kerasの画像処理モジュール(tf.imageなど)を用いてこれらの処理を実装された経験をお持ちかと思います。TensorFlow.jsでこれらの処理を実装する際、Pythonでの知識や経験をどのように活かせるか、あるいはどのような違いがあるのかを知ることは、スムーズな開発に繋がります。

この記事では、TensorFlow.jsを用いて一般的な画像前処理(リサイズ、クロップ、正規化)と後処理(推論結果の解釈、可視化準備)をどのように実装するかに焦点を当て、具体的なコード例とともに解説します。特に、Pythonでの実装方法と比較しながら、TensorFlow.jsにおける実装の考え方や注意点をご紹介します。

TensorFlow.jsにおける画像表現とテンソル操作の基本

TensorFlow.jsを含む多くの機械学習フレームワークでは、画像データは数値の多次元配列、すなわちテンソルとして扱われます。一般的なカラー画像の場合、テンソルは [高さ, 幅, チャンネル数] の形状を持つことが多く、各要素はピクセルの強度(例: 0〜255)を表します。TensorFlow.jsも同様に、画像をテンソルとして表現し、そのテンソルに対して様々な演算(オペレーション)を適用することで画像処理を行います。

ブラウザ上で画像データをTensorFlow.jsのテンソルに変換するには、tf.browser.fromPixels() 関数を使用するのが一般的です。この関数は、HTMLの <img>, <canvas>, <video> 要素や ImageData オブジェクトから直接テンソルを生成できます。

// HTMLの<img>要素からテンソルを生成する例
const imgElement = document.getElementById('myImage');
const imgTensor = tf.browser.fromPixels(imgElement);

console.log(imgTensor.shape); // 例: [480, 640, 3] (高さ, 幅, チャンネル数)
console.log(imgTensor.dtype); // 例: 'int32' (初期状態はint32またはfloat32)

// 処理後はメモリ解放が推奨されます
imgTensor.dispose();

PythonのTensorFlowやKerasでは、ファイルからの読み込みに tf.io.read_filetf.image.decode_image を使い、NumPy配列からの変換は tf.convert_to_tensor などを用いますが、TensorFlow.jsではWebのインターフェースと連携する tf.browser.fromPixels が中心的な役割を果たします。

画像処理の各ステップは、この画像テンソルに対してTensorFlow.jsが提供する様々なオペレーションを組み合わせて実現されます。

実践的な画像前処理の実装

モデルが学習時に使用した画像データの形式に合わせて、入力画像を変換するプロセスが前処理です。一般的な画像認識モデルでは、特定のサイズへのリサイズ、画像の一部分を切り出すクロップ、ピクセル値の正規化などが含まれます。

リサイズ (tf.image.resize)

多くの画像認識モデルは、固定された入力サイズを要求します(例: 224x224ピクセル)。入力画像のサイズが異なる場合、モデルに入力する前にリサイズが必要です。TensorFlow.jsでは tf.image.resize() 関数を使用します。

/**
 * 画像テンソルを指定したサイズにリサイズする関数
 * @param {tf.Tensor3D} imageTensor - 入力画像テンソル ([height, width, channels])
 * @param {number} targetHeight - リサイズ後の高さ
 * @param {number} targetWidth - リサイズ後の幅
 * @param {string} method - 補間方法 ('nearest', 'bilinear', 'bicubic')
 * @returns {tf.Tensor3D} リサイズされた画像テンソル
 */
function resizeImage(imageTensor, targetHeight, targetWidth, method = 'bilinear') {
  // tf.image.resizeはデフォルトでfloat32を返します
  const resizedTensor = tf.image.resize(imageTensor, [targetHeight, targetWidth], method);
  return resizedTensor;
}

// 使用例: 224x224にリサイズ (bilinear補間)
const originalTensor = tf.browser.fromPixels(document.getElementById('myImage')); // 例: [480, 640, 3]
const targetSize = 224;
const resizedTensor = resizeImage(originalTensor, targetSize, targetSize, 'bilinear');

console.log(resizedTensor.shape); // [224, 224, 3]
console.log(resizedTensor.dtype); // 'float32'

originalTensor.dispose();
resizedTensor.dispose();

PythonのTensorFlowでは tf.image.resize() が、OpenCVでは cv2.resize() が同様の機能を提供します。基本的な使い方は似ていますが、TensorFlow.jsの tf.image.resize は、デフォルトで入力テンソルを float32 に変換し、出力も float32 となる点に注意が必要です。Pythonの tf.image.resize も同様の挙動をしますが、NumPyやPIL/OpenCVではデータ型変換は明示的に行う必要があります。

クロップ/切り抜き (tf.image.cropAndResize, tf.slice)

画像の一部分だけをモデルに入力したい場合や、データ拡張としてランダムな領域を切り出したい場合にクロップを行います。よく使われる手法に、アスペクト比を保ったまま短辺に合わせてリサイズし、その後中央をクロップして正方形にする、というものがあります。

TensorFlow.jsでは tf.image.cropAndResize() や、より汎用的な tf.slice() を用いることができます。tf.image.cropAndResize は複数のバウンディングボックスを指定してクロップし、同時にリサイズを行う機能ですが、単一の画像を扱う場合でも利用可能です。tf.slice はテンソルの任意の次元からスライスを抽出する基本的なオペレーションです。

中央クロップの実装例を挙げます。

/**
 * 画像テンソルの中央を正方形にクロップする関数
 * @param {tf.Tensor3D} imageTensor - 入力画像テンソル ([height, width, channels])
 * @returns {tf.Tensor3D} クロップされた正方形の画像テンソル
 */
function centerCropImage(imageTensor) {
  const [height, width, channels] = imageTensor.shape;
  const size = Math.min(height, width); // 短辺のサイズ
  const startY = Math.floor((height - size) / 2); // クロップ開始Y座標
  const startX = Math.floor((width - size) / 2); // クロップ開始X座標

  // tf.sliceで指定範囲を切り抜く
  const croppedTensor = tf.slice(imageTensor, [startY, startX, 0], [size, size, channels]);

  return croppedTensor;
}

// 使用例: 中央クロップ
const originalTensor = tf.browser.fromPixels(document.getElementById('myImage')); // 例: [480, 640, 3]
const croppedTensor = centerCropImage(originalTensor);

console.log(croppedTensor.shape); // 例: [480, 480, 3] (短辺が480の場合)

originalTensor.dispose();
croppedTensor.dispose();

PythonのTensorFlowでは tf.image.crop_to_bounding_box、PILでは image.crop((left, top, right, bottom))、NumPyではスライシング(image[startY:startY+size, startX:startX+size])で同様の操作を行います。TensorFlow.jsの tf.slice はNumPyのスライシングに近い考え方でテンソル操作が可能です。

正規化/スケーリング (tf.div, tf.sub, tf.mul)

多くの事前学習済みモデルは、入力ピクセル値が特定の範囲(例: 0-1や-1-1)に正規化されているか、あるいは特定の平均・標準偏差で標準化されていることを期待します。tf.browser.fromPixels から得られるテンソルのdtypeは 'int32''float32' で、値の範囲は通常 0-255 です。これをモデルの入力要件に合わせて変換する必要があります。

基本的な正規化は、テンソルに対する算術演算を用いて行います。

/**
 * 画像テンソルのピクセル値を正規化/スケーリングする関数
 * @param {tf.Tensor3D} imageTensor - 入力画像テンソル ([height, width, channels]), dtypeはfloat32を想定
 * @param {string} method - 正規化方法 ('0-1', '-1-1', 'imagenet')
 * @returns {tf.Tensor3D} 正規化された画像テンソル
 */
function normalizeImage(imageTensor, method = '0-1') {
  // 入力テンソルがfloat32であることを確認/変換
  const floatImageTensor = imageTensor.toFloat();

  let normalizedTensor;
  switch (method) {
    case '0-1':
      // 0-255 -> 0-1 に正規化
      normalizedTensor = floatImageTensor.div(255.0);
      break;
    case '-1-1':
      // 0-255 -> -1-1 に正規化
      normalizedTensor = floatImageTensor.div(127.5).sub(1.0);
      break;
    case 'imagenet':
      // ImageNetで学習されたモデルに一般的な標準化
      // 各チャンネルの平均と標準偏差を引く
      // これらの値は学習に使われたデータセットによって異なります
      const mean = tf.tensor3d([123.68, 116.779, 103.939], [1, 1, 3]); // RGBチャンネルの平均 (ImageNetの場合の一例)
      normalizedTensor = floatImageTensor.sub(mean);
      // 標準偏差で割る場合もあります (例: VGG16/19やResNetなどでは平均のみ引くことが多い)
      // const std = tf.tensor3d([58.395, 57.12, 57.375], [1, 1, 3]);
      // normalizedTensor = normalizedTensor.div(std);
      break;
    default:
      throw new Error(`Unknown normalization method: ${method}`);
  }

  floatImageTensor.dispose(); // 中間テンソルを解放
  return normalizedTensor;
}

// 使用例: 0-1に正規化
const originalTensor = tf.browser.fromPixels(document.getElementById('myImage')).toFloat(); // まずfloatに変換
const normalizedTensor = normalizeImage(originalTensor, '0-1');

console.log(normalizedTensor.shape); // 例: [480, 640, 3]
console.log(normalizedTensor.dtype); // 'float32'
// テンソルの値を確認 (例: normalizedTensor.arraySync())

originalTensor.dispose();
normalizedTensor.dispose();

PythonのNumPyやTensorFlowでも、ピクセル値の正規化はテンソル(あるいはNumPy配列)に対する基本的な算術演算で行います。tensor / 255.0(tensor / 127.5) - 1.0 といった記述は、TensorFlow.jsでもほぼそのまま通用します。tf.math.mean, tf.math.std なども利用できます。ImageNetのような特定のデータセットで学習されたモデルでは、チャンネルごとの平均値を引くなどの処理が必要になることがあり、これもPythonと同様に、適切な平均・標準偏差テンソルを用意して演算することで実現できます。

画像後処理の実装

モデルの推論結果は通常、テンソルとして得られます。このテンソルは、分類モデルであれば各クラスの確率、オブジェクト検出モデルであればバウンディングボックスの座標とクラス確率、セグメンテーションモデルであれば各ピクセルのクラスラベルなどが含まれます。これらの数値を、人間が理解できる形や、次の処理ステップ(例: ブラウザ上での描画)に適した形に変換するのが後処理です。

推論結果の解釈

画像分類モデルの出力は、多くの場合、ソフトマックス関数を通過した各クラスの確率分布を示すテンソルです。最も確率の高いクラスや、確率の高い上位N個のクラスを取得するには、tf.argMax()tf.topk() といったオペレーションを使用します。

/**
 * 分類モデルの出力テンソルからトップKの予測結果を取得する関数
 * @param {tf.Tensor1D} predictionsTensor - モデルの出力テンソル (クラス数)
 * @param {number} k - 取得するトップKの数
 * @returns {Promise<{values: number[], indices: number[]}>} トップKの確率とそのインデックスを含むオブジェクト
 */
async function getTopKPredictions(predictionsTensor, k = 5) {
  // tf.topkはPromiseを返します(非同期処理)
  const topK = await tf.topk(predictionsTensor, k);

  // 結果のテンソルをJavaScriptのArrayに変換
  const topKValues = topK.values.arraySync();
  const topKIndices = topK.indices.arraySync();

  // テンソルは不要になったら解放
  topK.values.dispose();
  topK.indices.dispose();

  return { values: topKValues, indices: topKIndices };
}

// 使用例: 分類モデルの出力テンソルがあるとして
// const predictionsTensor = model.predict(inputTensor); // モデル推論で得られるテンソルを想定
// const top5 = await getTopKPredictions(predictionsTensor, 5);
// console.log("Top 5 predictions:", top5);

// ダミーの予測テンソルで試す場合
const dummyPredictions = tf.tensor1d([0.1, 0.05, 0.6, 0.02, 0.23]); // 例: 5クラス分類
getTopKPredictions(dummyPredictions, 3).then(top3 => {
  console.log("Top 3 predictions (dummy):", top3); // 例: { values: [ 0.6, 0.23, 0.1 ], indices: [ 2, 4, 0 ] }
});

dummyPredictions.dispose();

PythonのTensorFlow/KerasやNumPyでも、tf.argmax(), tf.math.top_k(), np.argmax(), np.argsort() などを用いて同様に予測結果を解釈します。TensorFlow.jsではブラウザ環境での実行を考慮し、多くの場合非同期処理(Promise)となる点に留意が必要です。

可視化のためのテンソル変換

推論結果をHTML Canvasなどの要素に描画したい場合、TensorFlow.jsのテンソルをJavaScriptのImageDataオブジェクトやピクセル配列(Uint8Arrayなど)に変換する必要があります。これは tf.browser.toPixels() 関数を用いて行えます。

/**
 * 画像テンソルをHTML Canvasに描画する関数
 * @param {tf.Tensor3D} imageTensor - 描画する画像テンソル ([height, width, channels])
 * @param {HTMLCanvasElement} canvas - 描画先のCanvas要素
 * @returns {Promise<void>} 描画完了を示すPromise
 */
async function drawTensorOnCanvas(imageTensor, canvas) {
  // tf.browser.toPixelsはAsyncです
  await tf.browser.toPixels(imageTensor, canvas);
}

// 使用例: 処理後の画像をCanvasに描画
// const processedTensor = yourPreprocessingFunction(originalTensor); // 前処理後のテンソル
// const outputCanvas = document.getElementById('outputCanvas');
// drawTensorOnCanvas(processedTensor, outputCanvas).then(() => {
//   console.log("Image drawn on canvas.");
//   processedTensor.dispose();
// });

// ダミーテンソルで描画を試す
const dummyImageTensor = tf.randomUniform([100, 150, 3], 0, 255).toInt(); // ランダムな100x150の画像データ (int型)
const outputCanvas = document.getElementById('outputCanvas'); // <canvas id="outputCanvas"></canvas> が必要
drawTensorOnCanvas(dummyImageTensor, outputCanvas).then(() => {
  console.log("Dummy image drawn on canvas.");
  dummyImageTensor.dispose();
});

Pythonでは、OpenCVの cv2.imshow() やPILの image.show()、あるいはMatplotlibなどのライブラリを用いて画像を可視化します。TensorFlow.jsでは、WebブラウザのDOM要素(Canvas)との連携が中心となります。特に tf.browser.toPixels は、テンソルのデータ型や値の範囲を考慮してCanvasに適した形式に変換してくれるため便利です。

パフォーマンスと注意点

ブラウザ環境での画像処理は、デスクトップやサーバーサイドと比較してリソースに制約がある場合があります。特に、高解像度な画像や複雑な処理を行う際は、パフォーマンスに注意が必要です。

まとめ

この記事では、TensorFlow.jsを用いた画像認識AI開発における実践的な画像前処理と後処理の実装方法を解説しました。リサイズ、クロップ、正規化といった一般的な前処理や、推論結果の解釈、可視化のための変換といった後処理は、Pythonでの開発経験をお持ちの方にとって馴染み深い概念かと思います。TensorFlow.jsでも、提供されている豊富なオペレーションを組み合わせることで、これらの処理をJavaScript環境で実現できます。

Pythonのコードと比較することで、TensorFlow.jsでの実装の考え方や、テンソルの扱い方、APIの類似点・相違点を理解しやすくなったかと思います。ブラウザ環境特有の非同期処理やメモリ管理に注意しながら、本記事のコード例を参考に、ご自身のプロジェクトに合わせた画像処理パイプラインを構築してみてください。

これらの基本を理解すれば、さらに複雑なデータ拡張や、カスタムな画像変換処理にも応用していくことが可能です。