Laravel で機械学習機能を Tensorflow.js によって実装 ②

# 前回作成した画像認識システムのコード詳細

今回は前回作成したコードの詳しい説明を書いていく。

## ビュー部分

まずはビュー部分から。

```php5
<form id="imageAnalysisForm" data-analyze-url="{{ route('image.analyze') }}" class="space-y-4">
                    @csrf
                    <input type="hidden" name="post_id" value="{{ $post->id }}">
                    
                    <div class="space-y-2">
                        <label for="ai_image" class="text-sm font-medium">分析する画像</label>
                        <input id="ai_image" type="file" name="image" class="w-full border border-gray-300 dark:border-gray-600 rounded-md text-sm file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
                    </div>
                    
                    <button type="submit" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md text-sm font-medium transition-colors">
                        分析する
                    </button>
                </form>
                
                <div id="analysisResult" class="mt-6 p-4 border border-gray-200 dark:border-gray-700 rounded-md hidden bg-gray-50 dark:bg-gray-900">
                    <h4 class="font-medium text-lg mb-2">分析結果</h4>
                    <div id="resultContent" class="text-sm"></div>
                </div>
            </div>
        </div>
    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.18.0"></script>
    <script src="{{ asset('js/tensorflow-classifier.js') }}"></script>
```

### 以下説明

```html
<form id="imageAnalysisForm" data-analyze-url="{{ route('image.analyze') }}" class="space-y-4">
```
フォームタグ。

**id="imageAnalysisForm"**
→ JavaScript でフォームを操作するためのID

**data-analyze-url="{{ route('image.analyze') }}"**
→ Laravel の route('image.analyze') で指定されたURLにデータを送る

```html
<input type="hidden" name="post_id" value="{{ $post->id }}">
```

**type="hidden"**
→ 画面には表示されないけど、データを送るための入力欄

**name="post_id"** → この名前で post_id を送る

**value="{{ $post->id }}** → 投稿IDをフォームに埋め込む

```html
<input id="ai_image" type="file" name="image" class="w-full border border-gray-300 dark:border-gray-600 rounded-md text-sm">
```

**type="file"** → 画像やファイルを選択するための入力欄

**name="image"** → Laravel に送るときのデータ名（image）

**id="ai_image"** → JavaScript で操作するためのID

```html
<div id="analysisResult" class="mt-6 p-4 border border-gray-200 dark:border-gray-700 rounded-md hidden bg-gray-50 dark:bg-gray-900">
    <h4 class="font-medium text-lg mb-2">分析結果</h4>
    <div id="resultContent" class="text-sm"></div>
</div>
```

```js
document.getElementById('analysisResult').classList.remove('hidden');
document.getElementById('resultContent').innerText = "これは猫の画像です！";
```

**id="analysisResult"**
→ 画像分析の結果を表示するエリア
hidden クラスがある！
→ 最初は非表示になっていて、後で表示される

**id="resultContent"**
→ 結果のテキストがここに入る
```js
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.18.0"></script>
```
この行は TensorFlow.js（テンソルフローJS） というライブラリを CDN から読み込むためのスクリプトです。

**TensorFlow.js**：Googleが開発した、JavaScriptで動く 機械学習ライブラリ

**CDN（Content Delivery Network）**：インターネット上のサーバーから 直接スクリプトをダウンロードして使う 方法

これを使うと、ブラウザ上でAI（機械学習モデル）を動かせるようになる

```js
<script src="{{ asset('js/tensorflow-classifier.js') }}"></script>
```
**asset('js/tensorflow-classifier.js')** は Laravel の public/js/tensorflow-classifier.js を参照する

このスクリプトで、画像を送信したり、分析結果を表示したりする

## ルーター部分
```php5
Route::post('/image/analyze', [ImageAnalysisController::class, 'analyze'])->name('image.analyze');
```

画像分析のフォーム **data-analyze-url="{{ route('image.analyze') }}"** に対応

✅ POST メソッドで 画像を送信 すると、このルートが実行される
✅ 対応するコントローラー： ImageAnalysisController@analyze

## コントローラー部分
まずはanalyze関数について

```php
public function analyze(Request $request)
    {
        // 基本的な画像分析のロジック
        $validated = $request->validate([
            'image' => 'required|file|mimes:jpeg,png,jpg,gif|max:2048',
            'post_id' => 'required|exists:posts,id'
        ]);
        
        $image = $request->file('image');
        $imageInfo = getimagesize($image->path());
        $width = $imageInfo[0];
        $height = $imageInfo[1];
        
        // 画像の分析結果
        $result = [
            'width' => $width,
            'height' => $height,
            'dominant_tone' => $this->getDominantTone($image->path()),
            'avg_color' => $this->getAverageColor($image->path()),
            'brightness' => $this->getBrightness($image->path()),
        ];
        
        return response()->json([
            'success' => true,
            'result' => $result
        ]);
    }
```

主要な色調 (dominant_tone) → getDominantTone()
平均色 (avg_color) → getAverageColor()
明るさ (brightness) → getBrightness()

また
success: true で成功を示す
result に分析結果を含める。

**response()->json([...])**

JSON（JavaScript Object Notation）というデータ形式でレスポンスを返す。
JSON はフロントエンドとサーバー間のデータのやり取りによく使われる形式


```php
// 主要な色調を取得
    private function getDominantTone($imagePath)
    {
        // 簡略化した実装
        $img = imagecreatefromstring(file_get_contents($imagePath));
        $w = imagesx($img);
        $h = imagesy($img);
        
        $rTotal = $gTotal = $bTotal = 0;
        $total = 0;
        
        for ($x = 0; $x < $w; $x += 10) {
            for ($y = 0; $y < $h; $y += 10) {
                $rgb = imagecolorat($img, $x, $y);
                $r = ($rgb >> 16) & 0xFF;
                $g = ($rgb >> 8) & 0xFF;
                $b = $rgb & 0xFF;
                
                $rTotal += $r;
                $gTotal += $g;
                $bTotal += $b;
                $total++;
            }
        }
        
        $rAvg = $rTotal / $total;
        $gAvg = $gTotal / $total;
        $bAvg = $bTotal / $total;
        
        if ($rAvg > $gAvg && $rAvg > $bAvg) {
            return '赤系';
        } elseif ($gAvg > $rAvg && $gAvg > $bAvg) {
            return '緑系';
        } elseif ($bAvg > $rAvg && $bAvg > $gAvg) {
            return '青系';
        } else {
            return 'モノクロ系';
        }
    }
```

関数の中身についてはあまり触れませんがみたとおりです（笑）

```php
// 平均色を取得
    private function getAverageColor($imagePath)
    {
        $img = imagecreatefromstring(file_get_contents($imagePath));
        $w = imagesx($img);
        $h = imagesy($img);
        
        $rTotal = $gTotal = $bTotal = 0;
        $total = 0;
        
        for ($x = 0; $x < $w; $x += 10) {
            for ($y = 0; $y < $h; $y += 10) {
                $rgb = imagecolorat($img, $x, $y);
                $r = ($rgb >> 16) & 0xFF;
                $g = ($rgb >> 8) & 0xFF;
                $b = $rgb & 0xFF;
                
                $rTotal += $r;
                $gTotal += $g;
                $bTotal += $b;
                $total++;
            }
        }
        
        $r = round($rTotal / $total);
        $g = round($gTotal / $total);
        $b = round($bTotal / $total);
        
        $hex = sprintf("#%02x%02x%02x", $r, $g, $b);
        
        return [
            'r' => $r,
            'g' => $g,
            'b' => $b,
            'hex' => $hex
        ];
    }
```
```php
// 明るさを取得
    private function getBrightness($imagePath)
    {
        $img = imagecreatefromstring(file_get_contents($imagePath));
        $w = imagesx($img);
        $h = imagesy($img);
        
        $brightnessTotal = 0;
        $total = 0;
        
        for ($x = 0; $x < $w; $x += 10) {
            for ($y = 0; $y < $h; $y += 10) {
                $rgb = imagecolorat($img, $x, $y);
                $r = ($rgb >> 16) & 0xFF;
                $g = ($rgb >> 8) & 0xFF;
                $b = $rgb & 0xFF;
                
                // 明るさの計算 (0-255)
                $brightness = (0.299 * $r + 0.587 * $g + 0.114 * $b);
                $brightnessTotal += $brightness;
                $total++;
            }
        }
        
        $avgBrightness = $brightnessTotal / $total;
        $category = '';
        
        if ($avgBrightness < 64) {
            $category = '暗い';
        } elseif ($avgBrightness < 128) {
            $category = 'やや暗い';
        } elseif ($avgBrightness < 192) {
            $category = 'やや明るい';
        } else {
            $category = '明るい';
        }
        
        return [
            'value' => round($avgBrightness, 2),
            'category' => $category
        ];
    }
}
```

## tensorflow-classifier.js 

まず中身は
```js
// TensorFlow.js Image Classifier for Laravel application
document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('imageAnalysisForm');
    const resultDiv = document.getElementById('analysisResult');
    const resultContent = document.getElementById('resultContent');
    
    // Model loading status
    let modelLoaded = false;
    let model;
    
    // Load the MobileNet model (pre-trained on ImageNet)
    async function loadModel() {
        try {
            // Load the model
            model = await tf.loadLayersModel('https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json');
            modelLoaded = true;
            console.log('MobileNet model loaded successfully');
            
            // Add a notification that the model is ready
            const modelStatus = document.createElement('div');
            modelStatus.className = 'text-green-600 mb-2';
            modelStatus.textContent = 'AI モデルの準備完了';
            form.insertBefore(modelStatus, form.firstChild);
        } catch (error) {
            console.error('Error loading model:', error);
        }
    }
    
    // Call model loading when page loads
    loadModel();
    
    // Process image and make prediction
    async function classifyImage(imgElement) {
        if (!modelLoaded) {
            return { error: 'モデルがまだ読み込まれていません。少々お待ちください。' };
        }
        
        try {
            // Preprocess the image to match MobileNet's input requirements
            const tfImg = tf.browser.fromPixels(imgElement)
                .resizeNearestNeighbor([224, 224]) // Resize to 224x224
                .toFloat()
                .div(tf.scalar(255.0))
                .expandDims();
            
            // Make prediction
            const predictions = await model.predict(tfImg).data();
            
            // Get the top 5 predictions
            const topPredictions = Array.from(predictions)
                .map((prob, index) => ({ probability: prob, className: IMAGENET_CLASSES[index] }))
                .sort((a, b) => b.probability - a.probability)
                .slice(0, 5);
            
            return {
                success: true,
                predictions: topPredictions
            };
        } catch (error) {
            console.error('Error classifying image:', error);
            return { error: '画像分類エラー: ' + error.message };
        } finally {
            // Clean up tensors
            tf.dispose();
        }
    }
    
    // Handle form submit
    form.addEventListener('submit', async function(e) {
        e.preventDefault();
        
        const imageInput = document.getElementById('ai_image');
        if (!imageInput.files || imageInput.files.length === 0) {
            resultContent.innerHTML = '<div class="text-red-600">画像を選択してください。</div>';
            resultDiv.classList.remove('hidden');
            return;
        }
        
        const file = imageInput.files[0];
        const formData = new FormData(form);
        formData.append('post_id', formData.get('post_id'));
        
        // Show loading state
        resultContent.innerHTML = '<div>分析中...</div>';
        resultDiv.classList.remove('hidden');
        
        // First, display the image
        const img = document.createElement('img');
        img.src = URL.createObjectURL(file);
        img.style.maxWidth = '300px';
        img.style.maxHeight = '300px';
        img.style.marginBottom = '10px';
        
        // When image is loaded, classify it
        img.onload = async function() {
            // Perform the existing image analysis
            try {
                const response = await fetch(form.getAttribute('data-analyze-url') || '{{ route("image.analyze") }}', {
                    method: 'POST',
                    body: formData,
                    headers: {
                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
                    }
                });
                
                const data = await response.json();
                
                // Now perform TensorFlow.js classification
                const tfResult = await classifyImage(img);
                
                // Combine the results
                let htmlContent = '';
                
                if (data.success) {
                    // Display standard image analysis results
                    let colorDisplay = '';
                    if (data.result.avg_color && data.result.avg_color.hex) {
                        colorDisplay = 
                            <div class="flex items-center mt-2">
                                <div style="background-color: ${data.result.avg_color.hex}; width: 50px; height: 50px; border: 1px solid #ccc;"></div>
                                <div class="ml-2">
                                    <div>色コード: ${data.result.avg_color.hex}</div>
                                    <div>RGB値: R=${data.result.avg_color.r}, G=${data.result.avg_color.g}, B=${data.result.avg_color.b}</div>
                                </div>
                            </div>
                        ;
                    }
                    
                    htmlContent += 
                        <div class="text-green-600">分析完了！</div>
                        <div class="mt-3">
                            <div><strong>画像サイズ:</strong> ${data.result.width} × ${data.result.height} ピクセル</div>
                            <div><strong>優勢な色調:</strong> ${data.result.dominant_tone}</div>
                            ${data.result.brightness ? <div><strong>明るさ:</strong> ${data.result.brightness.value} (${data.result.brightness.category})</div> : ''}
                        </div>
                        <div class="mt-3">
                            <strong>平均色:</strong>
                            ${colorDisplay}
                        </div>
                    ;
                }
                
                // Add TensorFlow.js classification results
                if (tfResult.success) {
                    htmlContent += 
                        <div class="mt-4">
                            <strong>AI画像分類結果:</strong>
                            <ul class="mt-2 list-disc pl-5">
                                ${tfResult.predictions.map(pred => 
                                    <li>${pred.className}: ${(pred.probability * 100).toFixed(2)}%</li>
                                ).join('')}
                            </ul>
                        </div>
                    ;
                } else if (tfResult.error) {
                    htmlContent += <div class="mt-4 text-red-600">AI分類エラー: ${tfResult.error}</div>;
                }
                
                // Display the image and results
                resultContent.innerHTML = 
                    <div class="mb-4">
                        <strong>分析した画像:</strong><br>
                        ${img.outerHTML}
                    </div>
                    ${htmlContent}
                ;
                
                // Save the analysis results if needed
                if (data.success && tfResult.success) {
                    const combinedResult = {
                        standardAnalysis: data.result,
                        aiClassification: tfResult.predictions
                    };
                    
                    saveAnalysisResults(combinedResult);
                }
                
            } catch (error) {
                resultContent.innerHTML = 
                    <div class="mb-4">
                        <strong>分析した画像:</strong><br>
                        ${img.outerHTML}
                    </div>
                    <div class="text-red-600">エラー: ${error.message}</div>
                ;
            }
        };
    });
    
    // Save analysis results to the server
    function saveAnalysisResults(combinedResult) {
        fetch('/image/save-analysis', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
            },
            body: JSON.stringify({
                post_id: document.querySelector('input[name="post_id"]').value,
                analysis_data: JSON.stringify(combinedResult)
            })
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                console.log('分析結果が保存されました');
            }
        })
        .catch(error => {
            console.error('分析結果の保存中にエラーが発生しました:', error);
        });
    }
});

 //This is a simplified version of ImageNet classes
 //In a real app, you'd want to include all 1000 classes
const IMAGENET_CLASSES = [
    'テレビ', '冷蔵庫', '本', 'タオル', '車', 'バス', '人', '犬', '猫', '鳥',
    '花', '木', '海', '山', '川', '空', '雲', '食べ物', '果物', '野菜',
    'コンピューター', 'カメラ', '時計', '電話', '椅子', 'テーブル', 'ベッド', 'ソファ', 'キッチン用品', '建物',
    '信号機', '道路', '橋', '船', '飛行機', '電車', '自転車', 'バイク', 'スポーツ用品', '楽器'];
```

### 以下説明

```js
async function loadModel() {
    try {
        model = await tf.loadLayersModel('https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json');
        modelLoaded = true;
        console.log('MobileNet model loaded successfully');
        
        // モデルがロードされたことをユーザーに通知
        const modelStatus = document.createElement('div');
        modelStatus.className = 'text-green-600 mb-2';
        modelStatus.textContent = 'AI モデルの準備完了';
        form.insertBefore(modelStatus, form.firstChild);
    } catch (error) {
        console.error('Error loading model:', error);
    }
}
```
**tf.loadLayersModel()** を使い、事前学習済みの MobileNet モデル をダウンロード
モデルがロードされたら **modelLoaded = true;** に設定
ロード完了後、「AIモデルの準備完了」メッセージを表示。
MobileNet とは事前学習済みの 画像認識モデル。
画像を入力すると、「何の画像か」を 確率付きで分類 できる
1000種類のカテゴリに分類可能（犬、猫、車、本 など） 

```js
async function classifyImage(imgElement) {
    if (!modelLoaded) {
        return { error: 'モデルがまだ読み込まれていません。少々お待ちください。' };
    }

    try {
        const tfImg = tf.browser.fromPixels(imgElement)
            .resizeNearestNeighbor([224, 224]) 
            .toFloat()
            .div(tf.scalar(255.0))
            .expandDims();

        const predictions = await model.predict(tfImg).data();

        const topPredictions = Array.from(predictions)
            .map((prob, index) => ({ probability: prob, className: IMAGENET_CLASSES[index] }))
            .sort((a, b) => b.probability - a.probability)
            .slice(0, 5);

        return { success: true, predictions: topPredictions };
    } catch (error) {
        console.error('Error classifying image:', error);
        return { error: '画像分類エラー: ' + error.message };
    } finally {
        tf.dispose();
    }
}

```

画像を前処理
**resizeNearestNeighbor([224, 224])** で MobileNetの入力サイズ（224×224）にリサイズ

**toFloat().div(tf.scalar(255.0))** で 0～1の範囲に正規化

**.expandDims()** で モデルの入力形状 (batch, height, width, channels) に合わせる

モデルに画像を渡して予測 **(model.predict(tfImg))**

確率が高い順に上位5つを取得 **(.slice(0, 5))**

エラーが発生したら catch で処理

メモリ解放 **(tf.dispose())**

```js
form.addEventListener('submit', async function(e) {
    e.preventDefault();

    const imageInput = document.getElementById('ai_image');
    if (!imageInput.files || imageInput.files.length === 0) {
        resultContent.innerHTML = '<div class="text-red-600">画像を選択してください。</div>';
        resultDiv.classList.remove('hidden');
        return;
    }

    const file = imageInput.files[0];
    const formData = new FormData(form);
    formData.append('post_id', formData.get('post_id'));

    resultContent.innerHTML = '<div>分析中...</div>';
    resultDiv.classList.remove('hidden');

    const img = document.createElement('img');
    img.src = URL.createObjectURL(file);
    img.style.maxWidth = '300px';
    img.style.maxHeight = '300px';

    img.onload = async function() {
        try {
            const response = await fetch(form.getAttribute('data-analyze-url') || '{{ route("image.analyze") }}', {
                method: 'POST',
                body: formData,
                headers: {
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
                }
            });

            const data = await response.json();
            const tfResult = await classifyImage(img);

            let htmlContent = '';
            if (data.success) {
                htmlContent += `<div><strong>画像サイズ:</strong> ${data.result.width} × ${data.result.height} ピクセル</div>`;
                htmlContent += `<div><strong>明るさ:</strong> ${data.result.brightness.value} (${data.result.brightness.category})</div>`;
            }

            if (tfResult.success) {
                htmlContent += `<div><strong>AI画像分類結果:</strong><ul>`;
                tfResult.predictions.forEach(pred => {
                    htmlContent += `<li>${pred.className}: ${(pred.probability * 100).toFixed(2)}%</li>`;
                });
                htmlContent += `</ul></div>`;
            }

            resultContent.innerHTML = htmlContent;
        } catch (error) {
            resultContent.innerHTML = `<div class="text-red-600">エラー: ${error.message}</div>`;
        }
    };
});

```
フォーム送信を無効化 (**e.preventDefault()**)
画像ファイルが選択されているかチェック
選択した画像をサーバー (**image.analyze**) に送信
サーバーの分析結果を取得
JavaScript（TensorFlow.js）でも分類
両方の結果を合成して表示

```js
function saveAnalysisResults(combinedResult) {
    fetch('/image/save-analysis', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
        },
        body: JSON.stringify({
            post_id: document.querySelector('input[name="post_id"]').value,
            analysis_data: JSON.stringify(combinedResult)
        })
    });
}
```
MobileNet を使って 画像分類
Laravel で 画像のサイズ・明るさを分析
両方の結果を統合して表示
サーバーに保存する機能付き