深度変化で物体の移動を検知する
定点カメラの映像から、AIで深度マップを推論して、深度が急激に変化した箇所にパーティクルを発生させる実験をした。
深度情報を過去と比較して、差分を検出。物体が動いた場所だけにエフェクトが出る。
やりたかったこと
防犯カメラとか監視カメラで、物体の移動を検知したい。でも、従来の画像の差分検出だと、照明が変わっただけで反応してしまう。
深度情報を使えば、実際に物体が動いたのか、光が変わっただけなのか、区別できるんじゃないかと思った。
深度が急激に変化した場所=物体が移動した場所。この仮説を試してみることにした。
深度マップの生成
midori276と同じMiDaSモデル(ONNX)を使った。WebGPUで512x512の解像度で推論。
class GPUInferenceManager {
constructor(modelPath) {
this.modelPath = modelPath;
this.session = null;
this.isWebGPUSupported = this._checkWebGPUSupport();
this.inferenceConfig = {
width: 512,
height: 512
};
}
async runInference(imageElement) {
if (!this.session) {
await this.loadModel();
}
// 推論実行
const feeds = { "input": tensor };
const results = await this.session.run(feeds);
const outputTensor = results["output"] || Object.values(results)[0];
const outputData = outputTensor.data;
// 最小/最大値を計算
let minVal = Infinity, maxVal = -Infinity;
for (let i = 0; i < outputData.length; i++) {
minVal = Math.min(minVal, outputData[i]);
maxVal = Math.max(maxVal, outputData[i]);
}
return { outputData, minVal, maxVal, width, height };
}
}
リアルタイムで深度マップを生成し続ける。約20fps出る。
過去データとの比較
深度データを毎フレーム保存して、前回のフレームと比較する。
let previousDepthData = null;
let lastDetectionTime = Date.now();
const detectionInterval = 80; // 80msごとに検出
function updateParticles() {
const currentDepthData = depthMapManager.getCurrentDepthData();
if (!currentDepthData) {
requestAnimationFrame(updateParticles);
return;
}
const now = Date.now();
if (previousDepthData && (now - lastDetectionTime > detectionInterval)) {
lastDetectionTime = now;
// 変化を検出してパーティクルを生成
detectChangesAndCreateParticles(currentDepthData, previousDepthData);
}
// 現在のデータを保存
previousDepthData = cloneDepthData(currentDepthData);
requestAnimationFrame(updateParticles);
}
80msごとに検出する。毎フレーム検出すると、パーティクルが出すぎる。
最初50msにしたら、パーティクルだらけになった。200msにしたら、逆に反応が遅い。80msがちょうど良かった。
変化量マップの作成
単純にピクセルごとの差分を取ると、ノイズだらけになる。一度つまずいた。
5x5の窓で平均化してから比較することにした。
function createChangeMap(currentData, previousData, width, height,
edgeThresholdX, edgeThresholdY, absoluteThreshold, relativeThreshold) {
const changeMap = new Float32Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// 映像の端は除外
if (x < edgeThresholdX || x > width - edgeThresholdX ||
y < edgeThresholdY || y > height - edgeThresholdY) {
continue;
}
const idx = y * width + x;
// 5x5の窓で平均を計算
const currentAvg = calculateAveragedValue(currentData, x, y, width, height, 2);
const previousAvg = calculateAveragedValue(previousData, x, y, width, height, 2);
// 変化量を計算
const absDiff = Math.abs(currentAvg - previousAvg);
const relDiff = previousAvg !== 0 ? absDiff / Math.abs(previousAvg) : 0;
// 閾値を超えたら記録
changeMap[idx] = (absDiff > absoluteThreshold || relDiff > relativeThreshold)
? absDiff * (1 + relDiff) : 0;
}
}
return changeMap;
}
平均化でノイズが減った。ヨシ。
端15%も除外してる。端は照明の影響を受けやすいから。
閾値の調整
変化を検出する閾値はthresholdFactorで調整する。
最初0.1にした。全然反応しない。手を動かしても、パーティクルが出ない。
0.01にしたら、今度はノイズに反応しまくる。何もしてないのにパーティクルが出る。これは課題。
0.045に落ち着いた。物体が動いた時だけ反応する。
const particleSettings = {
thresholdFactor: 0.045, // 深度変化の閾値係数
maxDetections: 20, // 1フレームあたりの最大検出数
samplingRate: 0.05, // サンプリングレート
edgeThresholdPercent: 15, // 端から除外する範囲
particleLifetime: 45, // パーティクルの寿命
particleSpeed: 2, // パーティクルの速度
particleSize: 3 // パーティクルのサイズ
};
thresholdFactorは0.001〜0.1の範囲で調整できる。GUIで動的に変えられるようにした。
グリッドベースの検出
全ピクセルをチェックすると負荷が高かった。グリッドに分割して、各セル内の最大変化点だけを検出する。
const gridCells = Math.round(25 * particleSettings.samplingRate * 10);
const cellWidth = width / gridCells;
const cellHeight = height / gridCells;
for (let cellY = 0; cellY < gridCells; cellY++) {
for (let cellX = 0; cellX < gridCells; cellX++) {
const centerX = Math.floor(cellX * cellWidth + cellWidth / 2);
const centerY = Math.floor(cellY * cellHeight + cellHeight / 2);
// セル内の最大変化点を検出
const maxChange = findMaxChangeInCell(changeMap, centerX, centerY, width, height,
cellWidth, cellHeight);
if (maxChange.value > 0) {
// パーティクルを生成
significantChanges.push({
x: maxChange.x,
y: maxChange.y,
score: maxChange.value
});
}
}
}
samplingRateが0.05だと、全体の5%をチェックする。これで十分。
0.2にしたら精度は上がるけど、CPUが負荷が高かった。0.01だと精度が下がって、小さい物体を見逃す。
0.05がバランス良かった。
パーティクルの3D配置
検出した変化点を3D空間にマッピングする。深度マップメッシュの座標系に合わせる必要があった。
function calculateExactPosition(x, y, normalizedDepth, width, height, depthMesh) {
const geometry = depthMesh.geometry;
const planeWidth = geometry.parameters.width;
const planeHeight = geometry.parameters.height;
// UV座標を計算
const uvX = x / width;
const uvY = y / height;
// メッシュ上の相対位置を計算
const localX = (uvX - 0.5);
const localY = (0.5 - uvY); // Y軸反転
// スケール
const scaledX = localX * planeWidth;
const scaledY = localY * planeHeight;
// 深度に基づくZ座標
const displacementScale = depthMesh.material.uniforms.displacementScale.value;
const scaledZ = -normalizedDepth * displacementScale;
return new THREE.Vector3(scaledX, scaledY, scaledZ);
}
深度値を使ってZ座標を計算。深度マップの凹凸と同じ位置にパーティクルが出る。
カラフルなパーティクル
深度値に応じて色を変えた。HSL色空間を使って、虹色のグラデーション。
// HSL色空間で色を生成
const hue = (depthValue * 360 + Math.random() * 60) % 360;
const saturation = 0.7 + Math.random() * 0.3;
const lightness = 0.5 + Math.random() * 0.3;
// HSLからRGBに変換
const rgb = hslToRgb(hue / 360, saturation, lightness);
particleColors[i * 3] = rgb[0]; // R
particleColors[i * 3 + 1] = rgb[1]; // G
particleColors[i * 3 + 2] = rgb[2]; // B
手前の物体は赤系、奥の物体は青系。深度が分かりやすい。
パフォーマンス
深度推論と変化検出を同時にやると負荷が高かった。
深度推論が20fps、変化検出の追加で15fpsまで落ちた。5x5窓の平均化が重かった。
3x3窓にしたら18fpsまで改善。でも、ノイズが増えた。
結局、5x5窓のまま、サンプリングレートを下げることにした。0.05で15fps、許容範囲だと判断した。
ハマったところ
ノイズとの戦い
最初、ノイズがひどかった。何もしてないのにパーティクルが出まくる。
原因は深度推論の精度。AIモデルの出力にはノイズが含まれてる。
5x5窓の平均化を入れて、ノイズが減った。でも完全には消えない。
閾値を上げすぎると、小さい変化を見逃す。下げすぎるとノイズに反応する。
何度も調整して、0.045に落ち着いた。
メモリの増加
深度データを毎フレームコピーしてたら、メモリが増え続けた。10秒で200MBとか。
cloneDepthDataでスプレッド演算子使ってた。これがダメだった。
// ❌ これだとメモリリーク
function cloneDepthData(data) {
return { ...data, outputData: [...data.outputData] };
}
// ✅ 必要な部分だけコピー
function cloneDepthData(data) {
return {
outputData: new Float32Array(data.outputData),
width: data.width,
height: data.height,
minVal: data.minVal,
maxVal: data.maxVal
};
}
Float32Arrayを使って、型付き配列で管理したら、メモリ使用量が安定した。
検出間隔の調整
最初、毎フレーム検出してた。パーティクルが出すぎて、画面が真っ白になった。
80msの間隔を入れた。これで適度な量になった。
50msだと多い、200msだと少ない。80msが最適だった。
使ってみて
実際に試してみてください:
デモを起動する(全画面表示)
定点映像で物体の移動を検知できた。深度情報を使うので、照明変化に強い。
ポイントは以下の4つ:
- 5x5窓で平均化してノイズ低減
- 閾値0.045で適切な感度を確保
- サンプリングレート0.05でパフォーマンス維持(約15fps)
- グリッドベース検出で処理を軽減
深度情報を使った物体移動検知は、従来の画像差分より安定してる。照明が変わっても誤検出しない。
ただし、処理が重いので、リアルタイムシステムとして使うには最適化が必要。スマホでは無理。
同じような深度ベースの物体検知を試している方の参考になれば嬉しいです。
まとめ
深度情報を過去と比較して、物体の移動を検知するシステムを作った。
ポイントは以下の5つ:
- MiDaSモデル(ONNX)で深度推論(WebGPU対応、512x512)
- 5x5窓で平均化してノイズ除去
- 閾値0.045、サンプリング0.05で最適化
- グリッドベース検出で高速化
- HSL色空間でカラフルなパーティクル表示
ノイズとの戦いが大変だったけど、最終的には安定した検出ができるようになった。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。