# テスト仕様書 (Test Specification): Pure Rust OnnxOCR

作成者: Shion Watanabe  
日付: 2025-11-09  
リポジトリ: http://github.com/siska-tech/pure-onnx-ocr

## 🎯 目的

  * プログラムの品質を保証するため、要件定義書、API設計書、詳細設計書に基づいたテスト戦略と具体的なテストケースを定義する。
  * 特に、プロジェクトの成否を左右する「Pure Rust」の制約 [4, 5, 6, 7] 下での推論エンジン（`tract`）の互換性という**最重要リスク**を早期に検証・発見する。
  * 各モジュール（前処理、後処理）のアルゴリズムが、リファレンス実装（Python + OpenCV/Pyclipper）のロジック [8, 9, 10, 11] をPure Rustクレート（`imageproc` [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 11, 24, 25], `i_overlay` [26, 27, 28, 29, 30, 17, 31, 32]）で正しく代替できていることを確認し、バグの早期発見とデグレード（意図しない機能低下）を防止する。

-----

### 1\. テスト方針

  * **実施するテストの種類**:

    1.  **技術検証 (PoC) テスト**:
          * 最優先で実施するGo/No-Goテスト。
          * `tract-onnx` [19, 33, 34] が、`PP-OCRv5 Server`の認識モデル（`rec.onnx`: SVTR\_HGNet [35, 36, 37]）をロード・実行できるかという、プロジェクトの**致命的リスク**（要求オペレータ `LayerNormalization` [7, 38, 39, 40, 41, 42] / `Scan` [38, 43] 等のサポート状況 [38]）を検証する。
    2.  **単体テスト (Unit Tests)**:
          * `cargo test` を使用し、各モジュールおよびプライベート関数（詳細設計書 3.0）のロジックを個別に検証する。
          * 特に、`preprocess_*` の画像リサイズ [44, 45, 46, 47, 48]・軸転置 [26, 49, 50, 51, 52, 53] ロジック、`postprocess_*` のポリゴンオフセット [27, 29] およびCTC Greedyデコード [54, 55, 16, 56, 31, 57, 58] のアルゴリズムを重点的にテストする。
    3.  **結合テスト (Integration Tests)**:
          * `tests/` ディレクトリに配置し、`cargo test` で実行する。
          * API設計書で定義された公開API（`OcrEngineBuilder`, `OcrEngine`）が、実際のモデルファイル（`.onnx`）と画像ファイルを使用して、E2E（End-to-End）で正しく動作することを検証する。

  * **テストカバレッジ（網羅率）の目標値**:

      * コアロジック（前処理、後処理、CTCデコード）を含むモジュール群において、**90%以上**を目標とする。

  * **使用するテストフレームワーク**:

      * Rust 標準のテストハーネス (`cargo test` および `#[test]` アトリビュート)。

### 2\. テスト環境

  * **OS**: Linux (Ubuntu 22.04+), macOS (12.0+), Windows (10, 11)
  * **言語バージョン**: Rust (Stable 1.70.0 以降)
  * **依存プログラム（テストアセット）**:
      * **テストモデル**: `jingsongliujing/OnnxOCR` [1] が推奨する `PP-OCRv5_Server-ONNX` [59, 35, 36, 37, 60, 61, 62] モデルセット。
          * `det.onnx` (DBNet [59, 63, 64, 65, 11])
          * `rec.onnx` (SVTR\_HGNet [35, 36, 37])
          * `ppocrv5_dict.txt` [1, 3, 38, 59, 35, 36, 66, 67, 68, 69, 70, 71]
      * **テスト画像**:
          * 高解像度の風景画像 (JPEG, PNG)
          * テキストが密集したドキュメント画像
          * テキストが含まれない画像（空白の画像）
          * 破損した画像ファイル

### 3\. テストケース

#### 3.1. 技術検証 (PoC) テスト

| テスト項目                                             | テスト条件                                                                             | 期待結果                                                                                                                                                                                                                                                          | 手順                               |
| :----------------------------------------------------- | :------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------- |
| **[PoC-01]** SVTR認識モデルのロード検証 (最重要リスク) | `OcrEngine::load_model` (内部) を `rec.onnx` (SVTR\_HGNet [35, 36, 37]) で実行する。   | `Ok(RunnableModel)` が返る。<br> **(失敗時)** `Err(OcrError::ModelLoadError)` が返る。この場合、`tract` [19, 33, 34] が要求オペレータ (例: `LayerNormalization` [7, 38, 39, 40, 41, 42]) をサポートしておらず、プロジェクトの**続行が困難**であることを意味する。 | `cargo test test_load_svtr_model`  |
| **[PoC-02]** DBNet検出モデルのロード検証               | `OcrEngine::load_model` (内部) を `det.onnx` (DBNet [59, 63, 64, 65, 11]) で実行する。 | `Ok(RunnableModel)` が返る。                                                                                                                                                                                                                                      | `cargo test test_load_dbnet_model` |

#### 3.2. 単体テスト (Unit Tests)

| テスト項目                                 | テスト条件                                                                            | 期待結果                                                                                                                                               | 手順                          |
| :----------------------------------------- | :------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------- |
| \*\*\*\* 検出前処理: リサイズ (長辺制限)   | 1920x1080 (W\>H) の画像を入力。<br>`det_limit_side_len = 960` [72, 73, 74, 75, 76]。  | リサイズ後の寸法が `(960, 540)` となる。<br>`scale_ratio` が `0.5` となる。                                                                            | `preprocess_detection(img)`   |
| \*\*\*\* 検出前処理: リサイズ (短辺画像)   | 800x600 の画像を入力。<br>`det_limit_side_len = 960`。                                | リサイズ後の寸法が `(800, 600)` となる。<br>`scale_ratio` が `1.0` となる。                                                                            | `preprocess_detection(img)`   |
| \*\*\*\* 検出前処理: テンソル変換          | `(960, 540)` にリサイズされた画像。                                                   | 出力テンソルの形状が ``(NCHW) [26, 49, 50, 51, 52, 53] となる。<br>テンソルの値が `0.0`～`1.0` [77, 48] に正規化 [26, 45, 78, 79, 80, 24] されている。 | `preprocess_detection(img)`   | | **** 検出後処理: 輪郭抽出 | `imageproc::find_contours` [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 11, 24, 25, 81] を使用し、既知の二値化マップ（`GrayImage`）を入力。 | 期待される数の `Contour` [12, 13, 14, 22] が返る。 | `postprocess_detection(...)` | | **** 検出後処理: ポリゴンオフセット | 既知の `geo_types::Polygon` [82, 83, 84, 29, 85] を `i_overlay::buffering` [26, 27, 29, 17, 31] に入力。<br>`det_unclip_ratio = 1.5` [35, 8, 9, 86, 10, 11]。 | ポリゴンが正しく拡大（オフセット）されている。 | `postprocess_detection(...)` | | **** 検出後処理: スケール復元 | 検出されたポリゴン座標（リサイズ後）と `scale_ratio = 0.5` を入力。 | ポリゴンの各座標が `1 / 0.5 = 2.0` 倍され、元画像スケールに正しく復元されている。 | `postprocess_detection(...)` | | **** 辞書ロード | 有効な `ppocrv5_dict.txt` [1, 3, 38, 59, 35, 36, 66, 67, 68, 69, 70, 71] のパスを入力。 | `Ok((Vec<String>, usize))` が返る。<br>`blank_id` [35, 55, 56] が `dictionary.len()` [55] と一致する。 | `load_dictionary(path)` | | **** CTC Greedyデコード (正常系) | 入力インデックス:``<br>`blank_id = 0` | 出力インデックス: `[1, 2, 3]` [54, 16, 31] | `ctc_greedy_decode(...)` 
| \*\*\*\* CTC Greedyデコード (重複なし)     | 入力インデックス: `[1, 2, 3]`<br>`blank_id = 0`                                       | 出力インデックス: `[1, 2, 3]`                                                                                                                          | `ctc_greedy_decode(...)`      |
| \*\*\*\* CTC Greedyデコード (ブランクのみ) | 入力インデックス: `` <br>`blank_id = 0`                                               | 出力インデックス:  `` (空のVec)                                                                                                                        | `ctc_greedy_decode(...)`      |
| \*\*\*\* 認識前処理: リサイズ (強制)       | 200x100 (W, H) のクロップ画像を入力。<br>`rec_image_shape = (320, 48)` [35, 36, 77]。 | `image::resize_exact` [46] により、アスペクト比が無視され `(320, 48)` にリサイズされる。                                                               | `preprocess_recognition(...)` |
| \*\*\*\* 認識前処理: バッチ化              | 10個のポリゴンを入力。<br>`rec_batch_size = 8`。                                      | `Vec<PreprocessedRecInput>` が返る。<br>1番目の要素: `batch_tensor` が `` 。<br>2番目の要素: `batch_tensor` が  ``。                                   | `preprocess_recognition(...)` |

#### 3.3. 結合テスト (Integration Tests)

| テスト項目                                   | テスト条件                                                                                       | 期待結果                                                                                                                                                                                        | 手順                                                  |
| :------------------------------------------- | :----------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------- |
| \*\*\*\* ビルダー (正常系)                   | `OcrEngineBuilder::build()` に、有効な `det.onnx`, `rec.onnx`, `dict.txt` のパスを指定する。     | `Ok(OcrEngine)` が返る。                                                                                                                                                                        | `OcrEngineBuilder::new()...build()`                   |
| \*\*\*\* ビルダー (異常系: モデルパス)       | `det.onnx` のパスに存在しないファイルを指定する。                                                | `Err(OcrError::IoError)` が返る。                                                                                                                                                               | `OcrEngineBuilder::new()...build()`                   |
| \*\*\*\* ビルダー (異常系: 辞書パス)         | `dict.txt` のパスに存在しないファイルを指定する。                                                | `Err(OcrError::IoError)` が返る。                                                                                                                                                               | `OcrEngineBuilder::new()...build()`                   |
| \*\*\*\* ビルダー (異常系: モデルロード失敗) | **[PoC-01]** が失敗した場合、`rec.onnx` のパスを指定する。                                       | `Err(OcrError::ModelLoadError)` が返る。                                                                                                                                                        | `OcrEngineBuilder::new()...build()`                   |
| \*\*\*\* E2E実行 (正常系: `run_from_path`)   | `OcrEngine` をビルドし、テキストを含む既知のテスト画像パスで `run_from_path` を実行する。        | `Ok(Vec<OcrResult>)` が返る。<br>`Vec` は空ではない。<br>返された `OcrResult.text` が画像の既知のテキストと一致する。<br>`OcrResult.bounding_box` [82, 83, 84, 87, 29, 85] が妥当な座標を持つ。 | `engine.run_from_path("tests/assets/test_image.jpg")` |
| \*\*\*\* E2E実行 (正常系: `run_from_image`)  | `image::open` で画像を `DynamicImage` [46, 47, 88] としてロードし、`run_from_image` で実行する。 | `Ok(Vec<OcrResult>)` が返る。<br>\*\*\*\* と同じテキスト結果が得られる。                                                                                                                        | `engine.run_from_image(&img)`                         |
| \*\*\*\* E2E実行 (正常系: テキストなし)      | テキストを一切含まない空白の画像で `run_from_path` を実行する。                                  | `Ok(Vec<OcrResult>)` が返る。<br>`Vec` は空 (`.is_empty() == true`)。                                                                                                                           | `engine.run_from_path("tests/assets/blank.jpg")`      |
| \*\*\*\* E2E実行 (異常系: 画像パス)          | 存在しない画像パスで `run_from_path` を実行する。                                                | `Err(OcrError::IoError)` が返る。                                                                                                                                                               | `engine.run_from_path("invalid_path.jpg")`            |
| \*\*\*\* E2E実行 (異常系: 破損画像)          | 破損した（画像としてデコードできない）ファイルで `run_from_path` を実行する。                    | `Err(OcrError::ImageError)` が返る。                                                                                                                                                            | `engine.run_from_path("tests/assets/corrupt.jpg")`    |