Google と Microsoft の OCR 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 はより正答率の高い解析が可能な半面、応答に少し時間がかかります。

動きが確認できるデモ画面を作りました。
API 呼び出しは動きませんが、キャッシュからの読み込みは動作します。

デモ画面を開く

参考: OCR API を利用した RPA の PHP プログラムサンプル


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

<?php
/**
 * OCRサービスインターフェース
 */

interface OCRService{

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

	/**
	 * すでに取得済みの画像解析結果を本インスタンスに設定する(キャッシュ読み込み用)。
	 *
	 * @param string $result 画像解析結果(レスポンス結果)
	 */
	public function setResult(string $result): void;

	/**
	 * 設定された画像解析結果を返す。
	 *
	 * @return string|null 画像解析結果
	 */
	public function getResult(): ?string;

	/**
	 * 抽出されたテキスト全文を返す。
	 *
	 * @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 呼び出しの際にこれを渡します。
返ってきた結果は内部に保持します。

setResult()
API 呼び出しの結果は各サービスともに JSON 形式で返ってきますが、これをそのまま string 型でセットします。
キャッシュから結果データを読み込む場合に使用します。

getResult()
API 呼び出しの結果やセットされたキャッシュデータを返します。

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

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

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


サービスのテンプレートを作成
各サービスとも処理の流れが似通っているので、抽象クラスによってアルゴリズムを規定しちゃいます。
GoF でいうところのテンプレートメソッドというやつですね。
各サービスクラスはテンプレートに従って抽象メソッドを実装します。
オブジェクト指向はこのように設計思想をプログラムに落とし込めるのが気持ちいいです。

<?php
/**
 * OCRサービステンプレート
 */

abstract class BaseService implements OCRService{

	// 画像解析結果
	protected $sResult = null; // 画像解析結果のJSONデータ(レスポンス結果)
	protected $sAllTexts = ''; // 画像解析結果から得たすべての文字
	protected $aElements = array();// 画像解析結果を要素ごとに配列にしたもの ["element1", "element2", ・・・]
	protected $aWords = array(); // 画像解析結果を単語ごとに配列にしたもの ["word1", "word2", ・・・]

	/**
	 * APIを呼び出し、レスポンス結果を返す。
	 *
	 * @param string $imgPath 画像ファイル名(フルパス指定)
	 * @return string|null 画像解析結果
	 * @throws Exception API呼び出しに失敗した場合
	 */
	abstract protected function api(string $imgPath): ?string;

	/**
	 * 画像解析結果を解析し、$sAllTexts、$aElements、$aWordsに変換する。
	 */
	abstract protected function parse(): void;

	// @Implements - OCRService
	final public function callAPI(string $imgPath): void{
		$this->sResult = $this->api($imgPath);
		$this->parse();
	}

	// @Implements - OCRService
	final public function setResult(string $result): void{
		$this->sResult = $result;
		$this->parse();
	}

	// @Implements - OCRService
	final public function getResult(): ?string{
		return $this->sResult;
	}

	// @Implements - OCRService
	final public function getAllTexts(): string{
		return $this->sAllTexts;
	}

	// @Implements - OCRService
	final public function getElements(): array{
		return $this->aElements;
	}

	// @Implements - OCRService
	final public function getWords(): array{
		return $this->aWords;
	}
}
?>

api()
実際の API 呼び出しの処理を実装します。
callAPI()から呼ばれ、レスポンスデータを返します。
callAPI()では、api()から返ってきた値をインスタンス変数に保持し、下記の解析メソッドを呼び出します。

parse()
レスポンス結果あるいはキャッシュデータを解析し、解析結果をインスタンス変数に保持します。
getAllTexts()getElements()getWords()はここで保持された結果を元に配列データを生成します。


サービスクラスの実装(Google Cloud Vision API 用)
Google Cloud Vision API 用のサービスクラスです。

<?php
/**
 * Google Cloud Vision APIサービス
 */

class GoogleService extends BaseService{

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

	// @Override - BaseService
	protected function api(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
			)
		);
		$result = 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(!$result){
			throw new Exception('API呼び出しでエラーが発生しました。結果がありません。');
		}
		$json = json_decode($result, true);
		if(($json)&&(array_key_exists('error', $json))){
			$msg = 'code='.$json['error']['code'].' msg='.$json['error']['message'];
			throw new Exception('API呼び出しでエラーが発生しました。'.$msg);
		}
		return $result;
	}

	// @Override - BaseService
	protected function parse(): 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;
			}
		}
	}
}
?>

api()
CURL で API を呼び出しています。
API キーのOCR_GOOGLE_APIKEYは別のファイル(サンプルだとdefines.inc)で定義しておきます。

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


サービスクラスの実装(Microsoft Computer Vision OCR API 用
Microsoft Computer Vision の OCR API 用のサービスクラスです。

<?php
 /**
 * Microsoft Computer Vision OCR APIサービス
 */

class MSOCRService extends BaseService{

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

	// @Override - BaseService
	protected function api(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
			)
		);
		$result = 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(!$result){
			throw new Exception('API呼び出しでエラーが発生しました。結果がありません。');
		}
		$json = json_decode($result, true);
		if(($json)&&(array_key_exists('error', $json))){
			$msg = 'code='.$json['error']['code'].' msg='.$json['error']['message'];
			throw new Exception('API呼び出しでエラーが発生しました。'.$msg);
		}
		return $result;
	}

	// @Override - BaseService
	protected function parse(): 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;
			}
		}
	}
}
?>

api()
CURL で API を呼び出しています。
API キーのOCR_MICROSOFT_APIKEYや、カスタムドメイン名のOCR_MICROSOFT_CUSTOMDOMAINは別のファイル(サンプルだとdefines.inc)で定義しておきます。

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


サービスクラスの実装(Microsoft Computer Vision Read API 用)
Microsoft Computer Vision の Read API 用のサービスクラスです。

<?php
 /**
 * Microsoft Computer Vision Read APIサービス
 */

class MSReadService extends BaseService{

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

	// @Override - OCRService
	protected function api(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
			)
		);

		$result = null;
		$loop = false;
		do{
			$result = 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($result, 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);
		return $result;
	}

	// @Override - BaseService
	protected function parse(): 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;
			}
		}
	}
}
?>

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

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


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

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

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


コメント