0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

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

ビュー部分

まずはビュー部分から。

<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>

以下説明

<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にデータを送る

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

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

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

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

<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

<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>
document.getElementById('analysisResult').classList.remove('hidden');
document.getElementById('resultContent').innerText = "これは猫の画像です!";

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

id="resultContent"
→ 結果のテキストがここに入る

<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(機械学習モデル)を動かせるようになる

<script src="{{ asset('js/tensorflow-classifier.js') }}"></script>

asset('js/tensorflow-classifier.js') は Laravel の public/js/tensorflow-classifier.js を参照する

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

ルーター部分

Route::post('/image/analyze', [ImageAnalysisController::class, 'analyze'])->name('image.analyze');

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

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

コントローラー部分

まずはanalyze関数について

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 はフロントエンドとサーバー間のデータのやり取りによく使われる形式

// 主要な色調を取得
    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 'モノクロ系';
        }
    }

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

// 平均色を取得
    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
        ];
    }
// 明るさを取得
    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

まず中身は

// 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 = [
    'テレビ', '冷蔵庫', '', 'タオル', '', 'バス', '', '', '', '',
    '', '', '', '', '', '', '', '食べ物', '果物', '野菜',
    'コンピューター', 'カメラ', '時計', '電話', '椅子', 'テーブル', 'ベッド', 'ソファ', 'キッチン用品', '建物',
    '信号機', '道路', '', '', '飛行機', '電車', '自転車', 'バイク', 'スポーツ用品', '楽器'];

以下説明

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種類のカテゴリに分類可能(犬、猫、車、本 など)

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())

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)でも分類
両方の結果を合成して表示

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 で 画像のサイズ・明るさを分析
両方の結果を統合して表示
サーバーに保存する機能付き

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?