ゼロから作るPHPとPythonの連携 on Laravel

#はじめに
PHPからPythonを実行して画像認識した結果を表示するソフトをLaravelベースで実装してみます。

画像認識の実装は[TensorFlowでMNISTの学習をしてみる](https://qiita.com/SwitchBlade/items/6677c283b2402d060cd0)で、学習とテストを分離したものを実装していたので、それをベースにします。

[2020/4/15]ビューにBootstrapを導入

#環境
- OS: Ubuntu 18.04.4 LTS (Virtual BOX上)
- PHP：7.2
- Laravel：7.5.2
- Python：3.8.0
- tensorflow：1.14.0
- numpy：1.18.2
- opencv：4.2.0.34

Ubuntu 18.04.4 LTSにデフォルトで入ってるPython3は3.6.9だったので、最新に上げています。併せて、Pythonで使うライブラリも入れます。

```sh
$sudo apt install python3.8
$sudo apt-get install python3-pip python3-dev
$pip3 install tensorflow
$pip3 install numpy
$pip3 install opencv-python
```
`$sudo apt-get install python-pip python-dev`と`pip`コマンドでインストールするとPython2のライブラリが入るので注意が必要。

"~/.bashrc"を書き換えて、python3.8を使うようにします。

``` ..bashrc
alias python3='/usr/bin/python3.8'
```

#プロジェクトの作成
laravelAIという名前のプロジェクトを作成します。

```console
$ composer create-project laravel/laravel laravelAI --prefer-dist
```

#ディレクトリ・ファイル構成
今回追加・修正するものだけ記載します。赤字が追加したものです。
laravelAI
　┗ app
　　┗ Http
　　　┗ <font color="DeepPink">MnistController.php</font>
　　┗ <font color="DeepPink">Python</font>：Python関連のファイルを格納
　　　┗ <font color="DeepPink">mnist_test.py</font>
　　　┗ <font color="DeepPink">DeepConvNet.py</font>
　　　┗ <font color="DeepPink">ckpt</font>：学習結果を格納
　┗ resources
　　┗ views
　　　┗ <font color="DeepPink">mnist</font>
　　　　┗ <font color="DeepPink">index.blade.php</font>
　┗ routes
　　┗ wab.php


#PHP (コントローラ)
コントローラーを作成します。

```console
$ php artisan make:controller MnistController
```

```MnistController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MnistController extends Controller
{
    public function index(Request $request)
    {
        $data = [
            'msg' => "画像を入力してください",
        ];
        return view('mnist.index', $data);
    }

    public function test(Request $request)
    {
        if(!empty($request->test_image))
        {
            // POSTで送信された画像を取得します
            $image = $request->file('test_image');
            // 保存先は"storage/app/public/image"になります
            // ファイル名は自動で割り振られます
            $up_pass = $image->store('public/image');
            $image_pass = "./storage/image/".basename($up_pass);
            // Pythonのファイルがあるパスを設定
            $pythonPath =  "../app/Python/";
            $command = "/usr/bin/python3 " . $pythonPath . "mnist_test.py " . $pythonPath . " " .$image_pass;
            exec($command , $outputs);

            //正規表現で結果行の抽出
            $results = preg_grep('/result:.*/' , $outputs);
            if(count($results) == 1){
                // 連想配列の先頭を取得
                $result = reset($results);
                $result = substr($result , strlen('result:') , 1 );
            } 
            else {
                $result = "解析に失敗しました。";
            }
            $data = [
                'msg' => "あってますか？",
                'image_pass' => $image_pass,
                'result'  => $result,
                'lines'   => count($outputs),
                'outputs' => $outputs,
            ];
        }
        else {
            $data = [
                'msg' => "画像がありません",
            ];
        }
        return view('mnist.index', $data);
    }
}
```

##説明
### Pythonコマンドの実行
```PHP
// Pythonのファイルがあるパスを設定
$pythonPath =  "../app/Python/";
$command = "/usr/bin/python3 " . $pythonPath . "mnist_test.py " . $pythonPath . " " .$image_pass;
exec($command , $outputs);
```
PHPからのコマンドの実行は、`exec()`関数を使用します。第1引数で実行するコマンドを指定し、第2引数にコマンドの出力結果を格納する変数を指定します。詳細は[ここ](https://www.php.net/manual/ja/function.exec.php)で。

`/usr/bin/python3  Pythonファイル`でPythonを実行します。
上記の`$command`の文字列は次のようになります。
`/usr/bin/python3 ../app/Python/mnist_test.py ../app/Python/ ./storage/image/FxY92Ji6j04cyozrx62yGHu9NQzLgsIqQq23YIcH.png `

コマンドを`$command = "pwd";` のように修正して実行すると、Laravelの実行パスは"/home/hogehoge/laravelAI/public"であることが分かります。それを前提にPythonファイルがある場所を指定します。

`mnist_test.py`の第1引数には「学習データのパス」、第2引数には「画像のパス」を指定するようにしています。

### 受信画像の保存場所に関して
```PHP
// POSTで送信された画像を取得します
$image = $request->file('test_image');
$up_pass = $image->store('public/image');
$image_pass = "./storage/image/".basename($up_pass);
```
画像の保存先は"storage/app/public/image"になります。
以下の[コマンド](https://laravel.com/docs/7.x/filesystem)で"public/storage"から"storage/app/public"にシンボリックリンクを張ります。`mnist_test.py`の第2引数、および、ビューで表示する画像は"public/storage"配下のパスを指定します。

```Console
$ php artisan storage:link
$ cd public
$ ls -la
＝＞ storage -> /home/hogehoge/laravelAI/storage/app/public
```

#PHP(ルーティング)
`routes/web.php`に以下を追加します。

``` routes/web.php
Route::get ('mnist', 'MnistController@index');
Route::post('mnist', 'MnistController@test');
```

#Pyhton
学習結果は[TensorFlowでMNISTの学習をしてみる](https://qiita.com/SwitchBlade/items/6677c283b2402d060cd0)のものをそのまま使用可能です。"Python/ckpt"フォルダに格納します。

テスト用のコードは少しだけ修正します。コマンドライン引数で「学習データのパス」と「画像のパス」を指定できるようにします。

``` mnist_test.py
# -*- coding: utf-8 -*-
print('mnist_test.py START')
import sys
import tensorflow as tf
import cv2
import numpy as np
import DeepConvNet as CNN
#コマンドライン引数を取得
args = sys.argv

IMAGE_SIZE  = 28    # 画像サイズ
NUM_CLASSES = 10    # 識別数

if __name__ == "__main__":
    tf.reset_default_graph()
    
    print('設定 START')
    # 式に用いる変数設定
    x_image = tf.placeholder("float", shape=[None, IMAGE_SIZE * IMAGE_SIZE])    # 入力
    y_label = tf.placeholder("float", shape=[None, NUM_CLASSES]) # 出力
    keep_prob = tf.placeholder("float")

    # モデルを作成
    logits = CNN.CNN.makeMnistCNN(x_image, keep_prob , IMAGE_SIZE , NUM_CLASSES)
    sess = tf.InteractiveSession()

    saver = tf.train.Saver()
    #変数を初期化して実行
    sess.run(tf.global_variables_initializer())
    print('設定 END')
    
    print('Restore Param Start')
    ckpt = tf.train.get_checkpoint_state(args[1] + 'ckpt')
    #print(os.path.exists(args[1] + 'ckpt'))
    if ckpt: # checkpointがある場合
        last_model = ckpt.model_checkpoint_path # 最後に保存したmodelへのパス
        print ("Restore load:" + last_model)
        saver.restore(sess, last_model) # 変数データの読み込み
    else:
        print('Restore Failed')
    print('Restore Param End')
    
    
    # 画像読み込み
    inputNum = 1
    for count in range(int(inputNum)):
        fileName =  args[2]
        print('fileName:' + fileName)

        # 初期化
        ximage = []
        
        # 画像読み込み
        image = cv2.imread(fileName, cv2.IMREAD_GRAYSCALE)
        if not image is None:
            image = cv2.resize(image, (IMAGE_SIZE, IMAGE_SIZE))
            ximage = image.flatten().astype(np.float32)/255.0
        else:
            print('Error:File Read Failed !!')

        if len(ximage)!=0:
            pred = np.argmax(logits.eval(feed_dict={x_image: [ximage], keep_prob: 1.0})[0])
            
            print("result:" + str(pred))
    sess.close()
    print('mnist_test.py END')
```

#PHP (ビュー)
ビューを作成します。レイアウトは適当です。

```index.blade.php　　//.html
<html>
<head>
    <title>Mnist</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" >
    <style>
        body{
            background-color: #EEFFEE;
        }
    </style>
</head>

<body>
<div class="container">
        <div class="row">
            <div class="col-sm-10">
                <h2>1-9の数字を認識します</h2>
                <p>{{$msg}}</p>

                <form method = "POST" action="/mnist" enctype="multipart/form-data">
                    @csrf
                    <input type="file" name="test_image"><BR>
                    <input type="submit" id="submit" value="送信">
                </form>
            </div>
        </div>

        <div class="row">
            @isset($image_pass)
                <div class="col-sm-5">
                    <h3>結果</h3>
                    <img src="{{ asset($image_pass)}}" width="112" height="112"> <BR>
                    @isset($result)
                        <p>この画像は「　{{$result}}　」です</p>
                    @endempty
                </div>
            @endempty

            @isset($outputs)
                <div class="col-sm-5">
                    <h3>Python出力( {{$lines}} 行)</h3>
                    @foreach ($outputs as $output)
                        {{$output}}<BR>
                    @endforeach
                </div>
            @endempty
        </div>
    </div>
</body>
</html>
```

# 動作確認
以下のコマンドでサーバを起動します。

```Console
$ php artisan serve 
```
ブラウザで"http://localhost:8000/mnist" にアクセスします。
![WS000226.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/3e8f7a24-b5e3-07de-e16c-05e1a28709be.jpeg)


ファイル選択して、送信します。
![WS000225.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/2d7737c8-3bed-f587-b115-cc556ac4787d.jpeg)


認識できました！

#終わりに
VueとTypeScriptを導入したものを[ゼロから作るPHPとPythonの連携 on Laravel + Vue + TypeScript](https://qiita.com/SwitchBlade/items/4ac15856f7309f9e082c)に記載しています。


#補足
##ファイル保存で例外： The "" file does not exist or is not readable. 
別件でコードをいじっているときに、ファイル保存の`$image->store()`で上記例外に行き当たったので捕捉を。

```例外
Symfony\Component\Mime\Exception\InvalidArgumentException
The "" file does not exist or is not readable. 
```

例外メッセージやコールスタックからは分かり辛いですが、アップロードしたファイルサイズの制限にひっかかっていることが原因です。

###対策
php.iniファイルにファイルサイズが定義されているので修正します。
(ネットで調べると.htaccessを修正する方法も記載されていましたが、`public/.htaccess`を修正しても改善しませんでした。)

```sh
# php.iniの場所を確認
$ php -i | grep php.ini
Configuration File (php.ini) Path => /etc/php/7.2/cli
Loaded Configuration File => /etc/php/7.2/cli/php.ini

$ sudo vi  /etc/php/7.2/cli/php.ini
```

以下の2行を必要に応じて修正します。PHP7.2では、デフォルトで2MB、8MBになっています。

```php.ini
; POSTでアップロードする全ファイルの合計の最大サイズ
post_max_size=2M
; 1ファイル当たりの最大サイズ
upload_max_filesize=8M
```

変更後はサーバを再起動させてください。

```console
$ php artisan  serve
```