Flaskでナンプレ解読アプリを作ってみた。

## はじめに
ナンプレの問題画像を読み込むと、解読して問題を解いてくれるアプリを作ってみました。

- 問題画像をアップロードすると、1マス毎の数字をOCRした結果が画面表示される。
- OCRしたナンプレ問題を、1マスずつ手入力で補正できる。
- ナンプレ問題の解答を取得して画面表示できる。

## 開発環境（一部）
- Python : 3.7.6
- Flask : 1.1.1
- Flask-Cors : 3.0.8
- Keras : 2.3.1
- tensorflow : 1.13.1
- opencv-python : 4.1.2.30

## ナンプレ画像の数値領域を抽出
ナンプレ問題の1マス毎の数値領域の画像取得は、OpenCVを利用しました。

まず、四角形の領域取得画像をグレースケールして、2値化します。

```
img_bgr = cv2.imread(image_path)
img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)

# 二値化
_, img_2chi = cv2.threshold(img_gray, 200, 255, cv2.THRESH_BINARY)

# 数独表の抽出
t_x, t_y, t_w, t_h, img_bgr, img_2chi = get_rect_sudoku(img_bgr, img_2chi, image_path)
```

次に、ナンプレ問題の正方形領域を検出します。

```
def get_rect_sudoku(image_bgr, image_2chi, target_path):
    image_bgr = cv2.copyMakeBorder(image_bgr, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=255)
    image_2chi = cv2.copyMakeBorder(image_2chi, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=255)

    contours, hierarchy = cv2.findContours(image_2chi, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
    area_list = list(map(lambda x: cv2.contourArea(x), contours))
    target_index = np.array(area_list).argsort()[::-1][1]  # 面積が2番目に大きい矩形のindex
    target_cnt = contours[target_index]
    x, y, w, h = 0, 0, 0, 0

    if target_cnt is not None:
        x, y, w, h = cv2.boundingRect(target_cnt)

    return x, y, w, h, image_bgr, image_2chi
```

後は、正方形領域を9×9等分して、ナンプレ問題の1マス毎の画像を取得します。


## 数字判定
手書き数字認識で有名なMNISTで学習したモデルを用いて、数字画像の判定を行いました。
CNNのアーキテクチャは覚えてませんが、どこからか拾ってきたものです。

```
model.add(Conv2D(32, (3, 3), padding="same", input_shape=(28, 28, 1)))
model.add(Activation("relu"))
model.add(Conv2D(32, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Conv2D(64, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(Conv2D(64, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Conv2D(128, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(Conv2D(128, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Flatten())
model.add(Dense(100))
model.add(Activation("relu"))
model.add(Dropout(0.5))
model.add(Dense(10))
model.add(Activation('softmax'))

model.compile(loss=categorical_crossentropy,
              optimizer='Adam',
              metrics=['accuracy'])
```



## ナンプレ問題の解答取得
ナンプレ問題を解くロジックも自分で作るのは大変なので以下を使わせて頂きました。
[クジラ飛行机 Pythonでナンプレを解いてみよう](https://news.mynavi.jp/article/zeropython-54/)

以下の考え方に基づいているらしいです。

1. 左上から右下へと順に空白マスを調べていく
2. 空白マスがあれば、その時点で配置可能な数字を調べる
3. 配置可能な数字を仮に配置して、次のマスを調べていく
4. もし配置がうまくいかなければ(3)に戻る
5. 最後のマスに到達するまで、(2)以降の処理を繰り返す
6. 最後に到達したら結果を出力する

## 画面イメージ
左がアップロードしたナンプレ問題画像で、右が1マス毎に数字判定した結果です。
そこそこ正しく、そこそこ失敗してます。

<img width="1033" alt="スクリーンショット 2020-04-24 21.36.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172876/c7204781-d499-5297-6254-9ac81ff53308.png">


## ソースコード
ソースコードはこちらにあります。
https://github.com/take9999/sudoku_ocr


## 以下を参考にさせて頂きました。

フロント部分は、ほぼ丸々使わせて頂いています。
https://qiita.com/Kazuya_Murakami/items/f5ef5fed850b8b9e7a81

Flask画像アップロード部分
https://qiita.com/keimoriyama/items/7c935c91e95d857714fb