オセロの石を数えたい！（画像処理，python, cv2, flask, rails）

# オセロの石を数えたい！
**背景**
最近（お正月休みに），オセロにハマりました。オンラインでの対戦では、置ける位置が可視化されたり、石の数を自動的にカウントして勝敗を判定する機能が備わっていて、とても便利です。しかし、せっかく対面でオセロを楽しめるときには、そうしたアシスト機能なしで、純粋な実力勝負をしたい！そんな思いから、久しぶりに実物のオセロ盤で対戦してみました。しかし、実際にプレイしてみると、ゲーム終了後に石の数を数えるのが意外と面倒に感じました。勝敗をスムーズに確認できれば、もっと快適に楽しめるのでは？と考え、画像処理の技術を使って自動的にオセロの石をカウントするアプリケーションを作成しました。このアプリケーションは、オセロ盤の写真を撮影すると、石の色と数を自動で識別し、最終的な勝敗を判定してくれます。**対面での対戦をより快適にしつつ、アナログの楽しさを損なわないツールです！**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/414241a1-451f-803c-625e-2e20ac1911c0.png" alt="qiita_othello_1">

## 概要
この記事では，PythonのFlaskフレームワークを用いた円検出Webアプリケーションの設計と実装について説明しています．本システムは，アップロードされた画像から円を検出し，その結果をクライアント側に返す機能を提供します．

## 使用技術
- **Python 3.9**
- **Flask** (Webサーバー)
- **OpenCV** (画像処理)
- **NumPy** (数値計算)
- **Pillow** (画像の読み込み)
- **Base64** (画像エンコーディング)
- **Flask-CORS** (クロスオリジンリソース共有対応)

## システムアーキテクチャ
このシステムは以下の2つのコンポーネントから構成されています:

1. **フロントエンド**
    - `new.html.erb`: ユーザーが画像をアップロードするためのインターフェース。

2. **バックエンド**
    - `Flask`を使用してWebサーバーを構築。
    - アップロードされた画像を処理し、円の数を検出。
    - 検出結果をJSON形式でクライアントに返送。


## 画像処理の流れ
所有しているPC上での動作を考え，古典的な画像処理で実現する．
1. **画像のリサイズ:** 画像サイズを512x512にリサイズ。
    👉️リサイズすることでアプロードした画像の解像度に囚われずに処理が可能． 
2. **グレースケール化:** 画像をグレースケールに変換。
3. **二値化:** しきい値処理で二値化。
    👉️二値化して白石の部分のみを抽出 / 黒石は盤面の緑色と（光の当たり方によって）区別が難しいため計算しない．
4. **ノイズ除去:** モルフォロジー演算を使用してノイズ除去。
5. **ガウシアンぼかし:** 平滑化処理でノイズをさらに除去。
6. **ハフ変換:** 円検出を実施。
    👉️白石を抽出
7. **結果の描画:** 検出結果の円を可視化。
## Python(Flask)コード構成

```python
from flask import Flask, request, jsonify, render_template
import cv2
import numpy as np
import io
from PIL import Image
import base64
from flask_cors import CORS
```

#### アプリケーションの初期化
```python
app = Flask(__name__)
CORS(app)
```
Flaskアプリケーションを初期化し、CORSを許可しています。
#### 円検出関数
```python
def detect_circles_from_image_file(image_file):
    image = Image.open(image_file.stream)
    image = np.array(image)
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    image = cv2.resize(image, (512, 512))

    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, binary_image = cv2.threshold(gray_image, 127, 255, cv2.THRESH_BINARY)
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (10, 10))
    cleaned_image = cv2.morphologyEx(binary_image, cv2.MORPH_OPEN, kernel)
    blurred_image = cv2.GaussianBlur(cleaned_image, (9, 9), 2)

    circles = cv2.HoughCircles(
        blurred_image,
        cv2.HOUGH_GRADIENT,
        dp=1.2,
        minDist=30,
        param1=30,
        param2=15,
        minRadius=20,
        maxRadius=32
    )

    circle_image = image.copy()
    if circles is not None:
        circles = np.uint16(np.around(circles))
        for circle in circles[0, :]:
            center = (circle[0], circle[1])
            radius = circle[2]
            cv2.circle(circle_image, center, 5, (0, 0, 255), -1)
            cv2.circle(circle_image, center, radius, (0, 255, 0), 5)

    _, buffer = cv2.imencode('.png', circle_image)
    encoded_image = base64.b64encode(buffer).decode('utf-8')

    return circles.shape[1] if circles is not None else 0, encoded_image
```
#### ルーティング
```python
@app.route('/')
def home():
    return render_template('index.html')

@app.route('/process_image', methods=['POST'])
def process_image():
    try:
        file = request.files['cropped_image']
        circle_count, circle_image = detect_circles_from_image_file(file)
        return jsonify({
            "status": "success",
            "white_count": circle_count,
            "black_count": 64 - circle_count,
            "difference": abs(circle_count - (64 - circle_count)),
            "circle_image": circle_image
        })
    except Exception as e:
        return jsonify({"status": "error", "message": str(e)})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5005, debug=True)
```
### 実行方法
1. 必要なライブラリのインストール:
    ```bash
    pip install flask opencv-python pillow numpy flask-cors
    ```
2. アプリケーションの起動:
    ```bash
    python app.py
    ```
3. ブラウザで `http://localhost:5005` にアクセスし、画像をアップロードして動作を確認します。

## 画像処理の結果
以下に示すように、白石を正確に検出できることが確認できます。
![qiita_othello_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/2254d503-60b3-0e58-b1d6-971ac88aff54.png)


## 改善点と考慮事項
- **閾値調整:** 円検出の精度向上のため、`param1`と`param2`の調整が必要。
- **エラー処理:** 例外処理の強化が必要。
    - 白石しかカウントしていないため（黒石=64-白石）、盤面に全て石が置かれている状態でなければ黒石の数を正確にカウントできない。


## Webアプリケーション
Webアプリケーションのフロントエンド部分を`ruby on rails`を用いて構築する．
new.html.erbを以下のようにプログラムした．（詳細は省略）
```html.erb
<div class="container">
    <!-- ファイルアップロードフォームとキャンバス -->
    <div class="left-section">
        <form action="/othello_count/upload" method="POST" enctype="multipart/form-data" class="upload-form">
          <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
          <input type="file" name="image" id="imageInput" accept="image/*" required class="file-input">
          <!--button type="submit" class="upload-button">画像をアップロード</button-->
        </form>
        <canvas id="cropCanvas" class="crop-canvas"></canvas>
        <button onclick="sendCroppedImage()" class="send-button">石をカウント</button>
    </div>

    <!-- 結果表示エリア -->
    <div class="right-section">
        <h2 class="results-title">カウント結果</h2>
        <div class="results-container">
            <div class="white-results-block">
                <p class="result-item-counter"><span id="whiteCount">-</span></p>
            </div>
            <div class="black-results-block">
                <p class="result-item-counter"><span id="blackCount">-</span></p>
            </div>
        </div>
        <p class="result-item"><strong>👉️石差:</strong> <span id="Difference">-</span></p>
    <img id="detectedCirclesImage" src="" alt="" class="detected-image">
    </div>
</div>
```
![スクリーンショット 2025-01-06 10.17.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/1d30c8f6-4143-8877-0eb3-b1e96bbc5039.png)
![スクリーンショット 2025-01-06 10.18.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/9aac4b40-52bd-b355-3e48-d7daf20fcb98.png)

## おわりに
今回は、FlaskとOpenCVを使用して画像内の円を検出し、オセロの盤面から石を数えるWebアプリケーションを構築しました。