so-manager-dev.com/app/Services/GoogleVisionService.php
2025-09-19 19:01:21 +09:00

332 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Http;
/**
* Google Cloud Vision API サービス
* SHJ-1本人確認自動処理でOCR処理に使用
*/
class GoogleVisionService
{
protected $apiKey;
protected $projectId;
protected $baseUrl = 'https://vision.googleapis.com/v1';
public function __construct()
{
$this->apiKey = config('shj1.apis.google_vision.api_key');
$this->projectId = config('shj1.apis.google_vision.project_id');
}
/**
* 画像からテキストを抽出OCR処理
*
* @param string $imageFilename 画像ファイル名
* @return string 抽出されたテキスト
* @throws Exception
*/
public function extractTextFromImage(string $imageFilename): string
{
try {
Log::info('GoogleVision OCR処理開始', [
'filename' => $imageFilename
]);
// 画像ファイルを取得
$imageData = $this->getImageData($imageFilename);
// Vision API リクエスト実行
$detectedText = $this->callVisionApi($imageData);
Log::info('GoogleVision OCR処理完了', [
'filename' => $imageFilename,
'text_length' => strlen($detectedText),
'text_preview' => substr($detectedText, 0, 100) . '...',
'text_full' => $detectedText, // ← 完整のOCR結果を追加
'debug_marker' => 'NEW_CODE_EXECUTED_' . time() // ← デバッグマーカー追加
]);
return $detectedText;
} catch (Exception $e) {
Log::error('GoogleVision OCR処理エラー', [
'filename' => $imageFilename,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* 画像データを取得Laravel Storage使用
*/
private function getImageData(string $filename): string
{
$disk = config('shj1.storage.photo_disk');
Log::info('GoogleVision 画像ファイル読取開始', [
'filename' => $filename,
'disk' => $disk
]);
if (!Storage::disk($disk)->exists($filename)) {
Log::error('GoogleVision 画像ファイルが見つかりません', [
'filename' => $filename,
'disk' => $disk
]);
throw new Exception("画像ファイルが見つかりません: {$filename}");
}
$imageContent = Storage::disk($disk)->get($filename);
if (empty($imageContent)) {
Log::error('GoogleVision 画像ファイルが空です', [
'filename' => $filename
]);
throw new Exception("画像ファイルが空です: {$filename}");
}
Log::info('GoogleVision 画像ファイル読取成功', [
'filename' => $filename,
'size' => strlen($imageContent)
]);
return base64_encode($imageContent);
}
/**
* Google Cloud Vision API呼び出し
*/
private function callVisionApi(string $base64Image): string
{
if (!$this->isApiKeyConfigured()) {
throw new Exception("Google Vision API キーが設定されていません");
}
$url = "{$this->baseUrl}/images:annotate";
$requestBody = [
'requests' => [
[
'image' => [
'content' => $base64Image
],
'features' => [
[
'type' => 'TEXT_DETECTION',
'maxResults' => 1
]
],
'imageContext' => [
'languageHints' => ['ja'] // 日本語優先
]
]
]
];
$response = Http::timeout(30)
->withHeaders([
'Content-Type' => 'application/json',
])
->post($url . '?key=' . $this->apiKey, $requestBody);
if (!$response->successful()) {
throw new Exception("Google Vision API request failed: " . $response->status());
}
$data = $response->json();
// APIエラーチェック
if (isset($data['responses'][0]['error'])) {
$error = $data['responses'][0]['error'];
throw new Exception("Google Vision API error: " . $error['message']);
}
// テキスト抽出結果を取得
$textAnnotations = $data['responses'][0]['textAnnotations'] ?? [];
if (empty($textAnnotations)) {
return ''; // テキストが検出されなかった場合
}
// 最初のアノテーションが全体のテキスト
return $textAnnotations[0]['description'] ?? '';
}
/**
* 文字列の類似度を計算
*
* @param string $expected 期待する文字列
* @param string $detected 検出された文字列
* @return float 類似度0-100
*/
public function calculateSimilarity(string $expected, string $detected): float
{
// 前処理:空白文字削除、大文字小文字統一
$expected = $this->normalizeText($expected);
$detected = $this->normalizeText($detected);
if (empty($expected) || empty($detected)) {
return 0.0;
}
// 複数の計算方法を試し、最高値を返す
$similarities = [];
// 1. 標準的な類似度計算
similar_text($expected, $detected, $percent);
$similarities[] = $percent;
// 2. 文字含有率計算OCRの分離文字対応
$containsScore = $this->calculateContainsScore($expected, $detected);
$similarities[] = $containsScore;
// 3. 部分文字列マッチング
$substringScore = $this->calculateSubstringScore($expected, $detected);
$similarities[] = $substringScore;
// 最高スコアを返す
return max($similarities);
}
/**
* 文字含有率スコア計算(姓名の分離文字対応)
*/
private function calculateContainsScore(string $expected, string $detected): float
{
if (empty($expected)) return 0.0;
// 期待文字列の各文字が検出文字列に含まれるかチェック
$expectedChars = mb_str_split($expected, 1, 'UTF-8');
$containedCount = 0;
foreach ($expectedChars as $char) {
if (mb_strpos($detected, $char, 0, 'UTF-8') !== false) {
$containedCount++;
}
}
return ($containedCount / count($expectedChars)) * 100;
}
/**
* 部分文字列マッチングスコア計算
*/
private function calculateSubstringScore(string $expected, string $detected): float
{
if (empty($expected)) return 0.0;
// 期待文字列を2文字以上のチャンクに分割してマッチング
$chunks = [];
$expectedLength = mb_strlen($expected, 'UTF-8');
// 2文字チャンク
for ($i = 0; $i <= $expectedLength - 2; $i++) {
$chunks[] = mb_substr($expected, $i, 2, 'UTF-8');
}
// 3文字チャンク
for ($i = 0; $i <= $expectedLength - 3; $i++) {
$chunks[] = mb_substr($expected, $i, 3, 'UTF-8');
}
if (empty($chunks)) return 0.0;
$matchedChunks = 0;
foreach ($chunks as $chunk) {
if (mb_strpos($detected, $chunk, 0, 'UTF-8') !== false) {
$matchedChunks++;
}
}
return ($matchedChunks / count($chunks)) * 100;
}
/**
* テキスト正規化OCR認識結果の形式差異に対応
*/
private function normalizeText(string $text): string
{
// 空白文字除去、改行除去
$text = preg_replace('/\s+/', '', $text);
// 全角英数字・記号を半角に変換
$text = mb_convert_kana($text, 'rnask', 'UTF-8');
// 住所の同義語統一
$text = str_replace(['東京市', '東京府'], '東京都', $text);
$text = str_replace(['大阪市', '大阪府'], '大阪', $text);
// 数字の統一(全角→半角は上でやっているが、明示的に処理)
$text = str_replace(['', '', '', '', '', '', '', '', '', ''],
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], $text);
// 記号の統一
$text = str_replace(['ー', '', '―'], '-', $text);
$text = str_replace(['の'], '', $text); // 「1番地の1」→「1番地1」
return $text;
}
/**
* 部分マッチング検索
*
* @param string $needle 検索する文字列
* @param string $haystack 検索対象の文字列
* @return bool
*/
public function containsText(string $needle, string $haystack): bool
{
$needle = $this->normalizeText($needle);
$haystack = $this->normalizeText($haystack);
return mb_strpos($haystack, $needle) !== false;
}
/**
* 画像形式の検証
*/
public function isValidImageFormat(string $filename): bool
{
$allowedExtensions = config('shj1.storage.allowed_extensions');
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
return in_array($extension, $allowedExtensions);
}
/**
* APIキーが設定されているかチェック
*/
public function isApiKeyConfigured(): bool
{
$dummyKey = env('GOOGLE_VISION_API_KEY') === 'dummy_google_vision_api_key_replace_with_real_one';
return !empty($this->apiKey) && !$dummyKey;
}
/**
* Vision API レスポンスから信頼度を取得
*/
public function getConfidenceScore(array $textAnnotation): float
{
if (!isset($textAnnotation['boundingPoly']['vertices'])) {
return 0.0;
}
// 検出領域のサイズから信頼度を推定
$vertices = $textAnnotation['boundingPoly']['vertices'];
if (count($vertices) < 4) {
return 0.0;
}
// 単純な信頼度計算(実際の実装では、より詳細な分析が必要)
return 0.95; // デフォルト高信頼度
}
}