Google と Microsoft の OCR サービス(Vision API)の PHP プログラム実装例

プログラミング

OCR(光学式文字認識)による画像からのテキスト検出・抽出するサービス(API)としては、Google の Cloud Vision と Microsoft の Computer Vision が有名ですね。
本記事ではこれら2つのサービスを利用した画像解析プログラムの PHP での実装方法を紹介します。

なお、こちらの記事内容が前提知識となります。

参考: 画像からテキストを検出する OCR サービス - Google Cloud Vision API
参考: 画像からテキストを検出する OCR サービス - Microsoft Computer Vision


機能設計
以下のような機能をサンプルプログラムに盛り込みます。

・画像をアップロードすると OCR サービスを利用してテキストを抽出する
・OCR サービスは Google と Microsoft を選択できるようにする
・画像解析結果を画面に表示する
・抽出されたテキストは、全体、要素ごと(段落や行など)、単語ごとの配列データに変換する
・同じ画像で何度も API を呼ばなくてすむように、結果をキャッシュできるようにする

なお、今回はサンプルという都合上、キャッシュできる画像は jpg ファイルのみとなっています。


画面設計
おおむね、こんな感じの画面で事足りるかなと思います。

画面設計

画面左側(黄色)が画像アップロードからの API 呼び出し、右側がキャッシュデータの読み込みです。
下部は解析元画像と結果の表示部分ですね。
呼び出せる API は Google Cloud Vision APIMicrosoft Computer Vision API になります。
Microsoft Computer Vision は、OCR API と Read API の2種類があります。
OCR API は少量のテキスト向きで、Read API はより正答率の高い解析が可能な半面、応答に少し時間がかかります。


クラス設計
各サービスクラスが実装する機能をインターフェースで定義します。

<?php
//
// サービスインターフェース
//
interface OCRService{

	/**
	 * テキスト抽出を行う画像ファイルを読み込み、APIを呼び出して結果を取得する。
	 * 何らかの原因で結果を取得できなかった場合は、nullを返すか例外を投げる。
	 *
	 * @param string $imgPath 画像ファイル名(パス指定)
	 * @return string|null 画像解析結果
	 * @throws Exception API呼び出しに失敗した場合
	 */
	public function callAPI(string $imgPath): ?string;

	/**
	 * 画像解析結果を設定する。
	 *
	 * @param string $result 画像解析結果
	 */
	public function setResult(string $result): void;

	/**
	 * 抽出されたテキスト全文を返す。
	 *
	 * @return string テキスト文字列
	 */
	public function getAllTexts(): string;

	/**
	 * 抽出されたテキストを要素ごとの配列データにして返す。
	 * (Googleでは段落データ、Microsoftでは行データ)
	 *
	 * @return array テキスト要素の配列 ["element1", "element2", ・・・];
	 */
	public function getElements(): array;

	/**
	 * 抽出されたテキストを単語ごとの配列データにして返す。
	 *
	 * @return array 単語の配列 ["word1", "word2", ・・・];
	 */
	public function getWords(): array;
}
?>



callAPI()
画像ファイルのパスを指定するとファイルを読み込んで画像データを取得し、API 呼び出しの際にこれを渡します。
API 呼び出しの結果は各サービスともに JSON 形式で返ってきますが、これをそのまま string 型で返します。

setResult()
API が返してきた結果をセットします。
キャッシュから結果データを読み込んできた場合などに使用します。
getAllTexts()getElements()getWords()はここでセットされた結果を元に配列データを生成します。

getAllTexts()
API が返してきた結果を解析し、抽出されたすべてのテキストを返します。

getElements()
API が返してきた結果を解析し、要素ごとの配列データに変換して返します。
Google では段落ごと、Microsoft では行ごとになります。

getWords()
API が返してきた結果を解析し、単語ごとの配列データに変換して返します。


サービスクラスの実装(Google Cloud Vision API 用)
CURL で API を呼び出しています。
API キーのOCR_GOOGLE_APIKEYは別のファイルで定義しておきます。

<?php
//
// Google Cloud Vision APIクラス
//
class GoogleOCR implements OCRService{

	// APIのURL
	private const APIURL = 'https://vision.googleapis.com/v1/images:annotate';

	// 画像解析結果
	private $sResult_ = null; // 画像解析結果のJSONデータ
	private $sAllTexts_ = ''; // 画像解析結果から得たすべての文字
	private $aElements_ = array();// 画像解析結果を要素ごとに配列にしたもの
	private $aWords_ = array(); // 画像解析結果を単語ごとに配列にしたもの

	// @Override - OCRService
	public function callAPI(string $imgPath): ?string{
		// POST Body
		$data = file_get_contents($imgPath);
		$req = array('requests'=>array(array(
			'image'=>array(
				'content'=>base64_encode($data),
			),
			'features'=>array(
				array(
					'type'=>'TEXT_DETECTION',
					'maxResults'=>10,
				),
			),
			'imageContext'=>array(
				'languageHints'=>array('ja'),
			)
		)));
		$json = json_encode($req);

		// リクエスト
		$curl = curl_init();
		curl_setopt_array(
			$curl,
			array(
				CURLOPT_URL => self::APIURL.'?key='.OCR_GOOGLE_APIKEY,
				CURLOPT_IPRESOLVE=>CURL_IPRESOLVE_V4,
				CURLOPT_HTTPHEADER=>array('Content-Type: application/json'),
				CURLOPT_POST=>true,
				CURLOPT_POSTFIELDS=>$json,
				CURLOPT_RETURNTRANSFER=>true,
				CURLOPT_TIMEOUT=>15
			)
		);
		$this->sResult_ = curl_exec($curl);
		$info = curl_getinfo($curl);
		curl_close($curl);

		// APIエラーチェック
		if(($info['http_code'] != 200)&&($info['http_code'] != 202)){
			throw new Exception('API呼び出しでエラーが発生しました。code='.$info['http_code']);
		}
		if(!$this->sResult_){
			throw new Exception('API呼び出しでエラーが発生しました。結果がありません。');
		}
		$json = json_decode($this->sResult_, true);
		if(($json)&&(array_key_exists('error', $json))){
			$msg = 'code='.$json['error']['code'].' msg='.$json['error']['message'];
			throw new Exception('API呼び出しでエラーが発生しました。'.$msg);
		}

		// 結果解析
		$this->matrix();
		return $this->sResult_;
	}

	// @Override - OCRService
	public function setResult(string $result): void{
		$this->sResult_ = $result;
		$this->matrix();
	}

	// @Override - OCRService
	public function getAllTexts(): string{
		return $this->sAllTexts_;
	}

	// @Override - OCRService
	public function getElements(): array{
		return $this->aElements_;
	}

	// @Override - OCRService
	public function getWords(): array{
		return $this->aWords_;
	}

	/**
	 * 画像解析結果を配列データに変換する。
	 */
	protected function matrix(): void{
		if(!$this->sResult_) return;

		$data = json_decode($this->sResult_, true);
		$anno = $data['responses'][0]['textAnnotations'];
		if(!$anno) return;

		// 全文
		$this->sAllTexts_ = $anno[0]['description'];

		// 単語
		$this->aWords_ = array();
		$len = count($anno);
		for($i=1; $i<$len; $i++){
			$this->aWords_[] = $anno[$i]['description'];
		}

		// 要素(段落)
		$this->aElements_ = array();
		$blocks = $data['responses'][0]['fullTextAnnotation']['pages'][0]['blocks'];
		if(!$blocks) return;
		foreach($blocks as $block){
			if(!array_key_exists('paragraphs', $block)) continue;
			$paragraphs = $block['paragraphs'];
			foreach($paragraphs as $paragraph){
				if(!array_key_exists('words', $paragraph)) continue;
				$words = $paragraph['words'];
				$text = '';
				foreach($words as $word){
					if(!array_key_exists('symbols', $word)) continue;
					$symbols = $word['symbols'];
					foreach($symbols as $symbol){
						$text .= $symbol['text'];
					}
				}
				$this->aElements_[] = $text;
			}
		}
	}
}
?>

matrix()
API が返してきた結果を本メソッドにて配列データなどに変換します。
API がどのような JSON 形式で結果を返してくるかがわかるかと思います。
Google Cloud Vision API では抽出したテキストをページ → ブロック → 段落 →  → 記号の階層構造に分けて返してくれますので、getElements()で段落ごとのテキストデータを返せるよう段落ごとにテキストをまとめました。


サービスクラスの実装(Microsoft Computer Vision OCR API 用
CURL で API を呼び出しています。
API キーのOCR_MICROSOFT_APIKEYや、カスタムドメイン名のOCR_MICROSOFT_CUSTOMDOMAINは別のファイルで定義しておきます。

<?php
//
// Microsoft Computer Vision OCR APIクラス
//
class MicrosoftOCR implements OCRService{

	// APIのURL
	private const APIURL = 'https://'.OCR_MICROSOFT_CUSTOMDOMAIN.'.cognitiveservices.azure.com/vision/v3.2/ocr';

	// 画像解析結果
	private $sResult_ = null; // 画像解析結果のJSONデータ
	private $sAllTexts_ = ''; // 画像解析結果から得たすべての文字
	private $aElements_ = array();// 画像解析結果を要素ごとに配列にしたもの
	private $aWords_ = array(); // 画像解析結果を単語ごとに配列にしたもの

	// @Override - OCRService
	public function callAPI(string $imgPath): ?string{
		// POST Body
		$data = file_get_contents($imgPath);

		// リクエスト
		$curl = curl_init();
		curl_setopt_array(
			$curl,
			array(
				CURLOPT_URL=>self::APIURL.'?language=ja&detectOrientation=true&model-version=latest',
				CURLOPT_IPRESOLVE=>CURL_IPRESOLVE_V4,
				CURLOPT_HTTPHEADER=>array(
					'Content-Type: application/octet-stream',
					'Ocp-Apim-Subscription-Key: '.OCR_MICROSOFT_APIKEY
		        ),
				CURLOPT_POST=>true,
				CURLOPT_POSTFIELDS=>$data,
				CURLOPT_RETURNTRANSFER=>true,
				CURLOPT_TIMEOUT=>15
			)
		);
		$this->sResult_ = curl_exec($curl);
		$info = curl_getinfo($curl);
		curl_close($curl);

		// APIエラーチェック
		if(($info['http_code'] != 200)&&($info['http_code'] != 202)){
			throw new Exception('API呼び出しでエラーが発生しました。code='.$info['http_code']);
		}
		if(!$this->sResult_){
			throw new Exception('API呼び出しでエラーが発生しました。結果がありません。');
		}
		$json = json_decode($this->sResult_, true);
		if(($json)&&(array_key_exists('error', $json))){
			$msg = 'code='.$json['error']['code'].' msg='.$json['error']['message'];
			throw new Exception('API呼び出しでエラーが発生しました。'.$msg);
		}

		$this->matrix();
		return $this->sResult_;
	}

	// @Override - OCRService
	public function setResult(string $result): void{
		$this->sResult_ = $result;
		$this->matrix();
	}

	// @Override - OCRService
	public function getAllTexts(): string{
		return $this->sAllTexts_;
	}

	// @Override - OCRService
	public function getElements(): array{
		return $this->aElements_;
	}

	// @Override - OCRService
	public function getWords(): array{
		return $this->aWords_;
	}

	/**
	 * 画像解析結果を配列データに変換する。
	 */
	protected function matrix(): void{
		if(!$this->sResult_) return;

		$data = json_decode($this->sResult_, true);
		$regions = $data['regions'];
		if(!$regions) return;

		$this->sAllTexts_ = '';
		$this->aElements_ = array();
		$this->aWords_ = array();
		foreach($regions as $reg){
			if(!array_key_exists('lines', $reg)) continue;
			$lines = $reg['lines'];
			foreach($lines as $line){
				if(!array_key_exists('words', $line)) continue;
				$words = $line['words'];
				$text = '';
				foreach($words as $word){
					$str = $word['text'];
					if(!$str) continue;
					$this->aWords_[] = $str;
					$this->sAllTexts_ .= $str;
					$text .= $str;
				}
				$this->aElements_[] = $text;
			}
		}
	}
}
?>

matrix()
Microsoft Computer Vision OCR API では抽出したテキストを区域(region) →  → 単語の階層構造に分けて返してくれますので、getElements()で行ごとのテキストデータを返せるよう行ごとにテキストをまとめました。


サービスクラスの実装(Microsoft Computer Vision Read API 用)
CURL で API を呼び出しています。
API キーのOCR_MICROSOFT_APIKEYや、カスタムドメイン名のOCR_MICROSOFT_CUSTOMDOMAINは別のファイルで定義しておきます。
Microsoft Computer Vsion Read API ではまず、テキスト検出の依頼だけを行う API を呼び出します。
この API は要求を受け付けた後すぐに復帰し、結果取得用の API の URL を返します。
この結果取得用の API を呼び出すことで画像解析の結果を取得できます。

<?php
//
// Microsoft Computer Vision Read APIクラス
//
class MicrosoftRead implements OCRService{

	// APIのURL
	private const APIURL = 'https://'.OCR_MICROSOFT_CUSTOMDOMAIN.'.cognitiveservices.azure.com/vision/v3.2/read/analyze';

	// 画像解析結果
	private $sResult_ = null; // 画像解析結果のJSONデータ
	private $sAllTexts_ = ''; // 画像解析結果から得たすべての文字
	private $aElements_ = array();// 画像解析結果を要素ごとに配列にしたもの
	private $aWords_ = array(); // 画像解析結果を単語ごとに配列にしたもの

	// @Override - OCRService
	public function callAPI(string $imgPath): ?string{
		// POST Body
		$data = file_get_contents($imgPath);

		// リクエスト
		$curl = curl_init();
		curl_setopt_array(
			$curl,
			array(
				CURLOPT_URL=>self::APIURL.'?language=ja&model-version=latest',
				CURLOPT_IPRESOLVE=>CURL_IPRESOLVE_V4,
				CURLOPT_HTTPHEADER=>array(
					'Content-Type: application/octet-stream',
					'Ocp-Apim-Subscription-Key: '.OCR_MICROSOFT_APIKEY
		        ),
				CURLOPT_HEADER=>true,
				CURLOPT_POST=>true,
				CURLOPT_POSTFIELDS=>$data,
				CURLOPT_RETURNTRANSFER=>true,
				CURLOPT_TIMEOUT=>15
			)
		);
		$res = curl_exec($curl);
		$info = curl_getinfo($curl);
		curl_close($curl);

		// APIエラーチェック
		if(($info['http_code'] != 200)&&($info['http_code'] != 202)){
			throw new Exception('API呼び出しでエラーが発生しました。code='.$info['http_code']);
		}
		$header = substr($res, 0, $info['header_size']);
		$payload = substr($res, $info['header_size']);
		$json = null;
		if($payload) $json = json_decode($payload, true);
		if(($json)&&(array_key_exists('error', $json))){
			$msg = 'code='.$json['error']['code'].' msg='.$json['error']['message'];
			throw new Exception('API呼び出しでエラーが発生しました。'.$msg);
		}

		// 結果取得URL
		$spos = strpos($header, 'Operation-Location: ') + strlen('Operation-Location: ');
		$epos = strpos($header, "\n", $spos);
		if($spos === false){
			throw new Exception('API呼び出しでエラーが発生しました。Operation-Locationが見つかりません。');
		}
		$location = trim(substr($header, $spos, $epos-$spos));

		// 結果を取得
		$curl = curl_init();
		curl_setopt_array(
			$curl,
			array(
				CURLOPT_URL=>$location,
				CURLOPT_IPRESOLVE=>CURL_IPRESOLVE_V4,
				CURLOPT_CUSTOMREQUEST=>'GET',
				CURLOPT_HTTPHEADER=>array(
					'Ocp-Apim-Subscription-Key: '.OCR_MICROSOFT_APIKEY
		        ),
				CURLOPT_RETURNTRANSFER=>true,
				CURLOPT_TIMEOUT=>15
			)
		);

		$loop = false;
		do{
			$this->sResult_ = curl_exec($curl);
			$info = curl_getinfo($curl);
			if(($info['http_code'] != 200)&&($info['http_code'] != 202)){
				throw new Exception('API呼び出しでエラーが発生しました。code='.$info['http_code']);
			}
			$json = json_decode($this->sResult_, true);
			if(!array_key_exists('status', $json)){
				throw new Exception('API呼び出しでエラーが発生しました(no status)。');
			}
			if($json['status'] == 'running'){
				$loop = true;
			}else if($json['status'] == 'succeeded'){
				$loop = false;
			}else{
				throw new Exception('API呼び出しでエラーが発生しました(no status)。');
			}
			sleep(3);
		}while($loop);
		curl_close($curl);

		$this->matrix();
		return $this->sResult_;
	}

	// @Override - OCRService
	public function setResult(string $result): void{
		$this->sResult_ = $result;
		$this->matrix();
	}

	// @Override - OCRService
	public function getAllTexts(): string{
		return $this->sAllTexts_;
	}

	// @Override - OCRService
	public function getElements(): array{
		return $this->aElements_;
	}

	// @Override - OCRService
	public function getWords(): array{
		return $this->aWords_;
	}

	/**
	 * 画像解析結果を配列データに変換する。
	 */
	protected function matrix(): void{
		if(!$this->sResult_) return;

		$data = json_decode($this->sResult_, true);
		$analy = $data['analyzeResult'];
		if(!$analy) return;
		if(!array_key_exists('readResults', $analy)) return;
		$results = $analy['readResults'];

		$this->sAllTexts_ = '';
		$this->aElements_ = array();
		$this->aWords_ = array();
		foreach($results as $result){
			if(!array_key_exists('lines', $result)) continue;
			$lines = $result['lines'];
			foreach($lines as $line){
				if(!array_key_exists('words', $line)) continue;
				$words = $line['words'];
				$text = '';
				foreach($words as $word){
					$str = $word['text'];
					if(!$str) continue;
					$this->aWords_[] = $str;
					$this->sAllTexts_ .= $str;
					$text .= $str;
				}
				$this->aElements_[] = $text;
			}
		}
	}
}
?>

matrix()
Microsoft Computer Vision OCR API では抽出したテキストをページ →  → 単語の階層構造に分けて返してくれますので、getElements()で行ごとのテキストデータを返せるよう行ごとにテキストをまとめました。


サンプルプログラム一式
下記からダウンロードできます。
ダウンロードした zip ファイルをドキュメントルート上に展開し、以下の define 値を設定すると動くかと思います。
キャッシュディレクトリのアクセス権に注意してください。
zip パスワードは testocr です。

// API Key & カスタムドメイン
define('OCR_GOOGLE_APIKEY', '');
define('OCR_MICROSOFT_APIKEY', '');
define('OCR_MICROSOFT_CUSTOMDOMAIN', '');

// キャッシュデータを保存するディレクトリ
define('OCR_CACHE_PATH', 'cache');


コメント