{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "# 程式特色\n",
        "1.使用MNIST和EMINIST及SVHN三個DATASET\n",
        "\n",
        "2.檢測到已處理好的資料集，直接從 Google Drive 秒速載入！\n",
        "\n",
        "3.有googel drive就不再下載data set訓練了\n",
        "\n",
        "4使用google drive以加快已訓練好的模型權重載入\n",
        "\n",
        "5.使用gradio顯示"
      ],
      "metadata": {
        "id": "UHDi7CALC1bj"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# 儲存格 1：環境設定、掛載 Drive 與資料集快取處理\n",
        "這段程式碼會在第一次執行時花點時間下載並轉換資料，但最後會把處理好的 Numpy 陣列存到你的 Google Drive。下次執行時，它就會直接從 Drive 載入，只要幾秒鐘！\n",
        "\n",
        "Python"
      ],
      "metadata": {
        "id": "C7vRLhsYIsiN"
      }
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ADOFujykH9rN",
        "outputId": "4c97169a-65d3-43aa-f06f-a1d8e4cff159"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "⏳ 正在掛載 Google Drive...\n",
            "Mounted at /content/drive\n",
            "\n",
            "⏳ 步驟 1: 載入或準備融合資料集...\n",
            "✅ 檢測到已處理好的資料集，直接從 Google Drive 秒速載入！\n",
            "\n",
            "✅ 資料準備完畢！\n",
            " 融合訓練集數量: 140000 筆\n",
            " 融合測試集數量: 30000 筆\n"
          ]
        }
      ],
      "source": [
        "import os\n",
        "import io\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "import matplotlib.pyplot as plt\n",
        "from PIL import Image, ImageOps\n",
        "from sklearn.metrics import classification_report\n",
        "\n",
        "import tensorflow as tf\n",
        "import tensorflow_datasets as tfds\n",
        "from tensorflow.keras import layers, models\n",
        "from google.colab import output, files, drive\n",
        "from IPython.display import HTML, display\n",
        "\n",
        "# 1. 掛載 Google Drive\n",
        "print(\"⏳ 正在掛載 Google Drive...\")\n",
        "drive.mount('/content/drive')\n",
        "\n",
        "# 2. 設定儲存路徑 (在你的 Google Drive 中建立一個專屬資料夾)\n",
        "DRIVE_DIR = '/content/drive/MyDrive/Digit_Recognition_Colab'\n",
        "os.makedirs(DRIVE_DIR, exist_ok=True)\n",
        "\n",
        "DATA_PATH = os.path.join(DRIVE_DIR, 'processed_dataset.npz')\n",
        "MODEL_PATH = os.path.join(DRIVE_DIR, 'mnist_emnist_svhn_model.keras') # 使用官方推薦的 .keras 格式\n",
        "\n",
        "print(\"\\n⏳ 步驟 1: 載入或準備融合資料集...\")\n",
        "\n",
        "# 3. 檢查是否已經有處理好的資料集\n",
        "if os.path.exists(DATA_PATH):\n",
        "    print(\"✅ 檢測到已處理好的資料集，直接從 Google Drive 秒速載入！\")\n",
        "    data = np.load(DATA_PATH)\n",
        "    x_train, y_train = data['x_train'], data['y_train']\n",
        "    x_test, y_test = data['x_test'], data['y_test']\n",
        "else:\n",
        "    print(\"⚠️ 未檢測到快取資料，開始下載與轉換資料集 (此步驟較久，但只需執行一次)...\")\n",
        "\n",
        "    # 載入 MNIST\n",
        "    (x_m_train, y_m_train), (x_m_test, y_m_test) = tf.keras.datasets.mnist.load_data()\n",
        "    x_m_train = x_m_train.reshape(-1, 28, 28, 1) / 255.0\n",
        "    x_m_test = x_m_test.reshape(-1, 28, 28, 1) / 255.0\n",
        "\n",
        "    # 載入 EMNIST\n",
        "    emnist_train = tfds.load(\"emnist/digits\", split=\"train\", as_supervised=True)\n",
        "    emnist_test = tfds.load(\"emnist/digits\", split=\"test\", as_supervised=True)\n",
        "\n",
        "    x_e_train, y_e_train = [], []\n",
        "    for img, label in emnist_train.take(40000):\n",
        "        x_e_train.append(tf.image.transpose(img).numpy())\n",
        "        y_e_train.append(label.numpy())\n",
        "    x_e_train, y_e_train = np.array(x_e_train) / 255.0, np.array(y_e_train)\n",
        "\n",
        "    x_e_test, y_e_test = [], []\n",
        "    for img, label in emnist_test.take(10000):\n",
        "        x_e_test.append(tf.image.transpose(img).numpy())\n",
        "        y_e_test.append(label.numpy())\n",
        "    x_e_test, y_e_test = np.array(x_e_test) / 255.0, np.array(y_e_test)\n",
        "\n",
        "    # 載入 SVHN\n",
        "    svhn_train = tfds.load(\"svhn_cropped\", split=\"train\", as_supervised=True)\n",
        "    svhn_test = tfds.load(\"svhn_cropped\", split=\"test\", as_supervised=True)\n",
        "\n",
        "    x_s_train, y_s_train = [], []\n",
        "    for img, label in svhn_train.take(40000):\n",
        "        img_res = tf.image.resize(tf.image.rgb_to_grayscale(img), [28, 28])\n",
        "        x_s_train.append(img_res.numpy())\n",
        "        y_s_train.append(label.numpy())\n",
        "    x_s_train, y_s_train = np.array(x_s_train) / 255.0, np.array(y_s_train)\n",
        "\n",
        "    x_s_test, y_s_test = [], []\n",
        "    for img, label in svhn_test.take(10000):\n",
        "        img_res = tf.image.resize(tf.image.rgb_to_grayscale(img), [28, 28])\n",
        "        x_s_test.append(img_res.numpy())\n",
        "        y_s_test.append(label.numpy())\n",
        "    x_s_test, y_s_test = np.array(x_s_test) / 255.0, np.array(y_s_test)\n",
        "\n",
        "    # 合併資料\n",
        "    x_train = np.concatenate([x_m_train, x_e_train, x_s_train], axis=0)\n",
        "    y_train = np.concatenate([y_m_train, y_e_train, y_s_train], axis=0)\n",
        "    x_test = np.concatenate([x_m_test, x_e_test, x_s_test], axis=0)\n",
        "    y_test = np.concatenate([y_m_test, y_e_test, y_s_test], axis=0)\n",
        "\n",
        "    print(\"\\n💾 正在將處理好的融合資料集壓縮並儲存到 Google Drive...\")\n",
        "    np.savez_compressed(DATA_PATH, x_train=x_train, y_train=y_train, x_test=x_test, y_test=y_test)\n",
        "    print(\"✅ 資料集儲存完畢！下次重開機將直接載入。\")\n",
        "\n",
        "print(f\"\\n✅ 資料準備完畢！\")\n",
        "print(f\" 融合訓練集數量: {x_train.shape[0]} 筆\")\n",
        "print(f\" 融合測試集數量: {x_test.shape[0]} 筆\")"
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# 儲存格 2：模型建構與快取訓練\n",
        "\n",
        "這段加入了 Keras 新版的 Input 層以消除警告，並把模型永久保存在你的雲端硬碟中。"
      ],
      "metadata": {
        "id": "8svSxROwIzrk"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "print(\"⏳ 步驟 2: 模型建構與訓練...\\n\")\n",
        "\n",
        "if os.path.exists(MODEL_PATH):\n",
        "    print(\"✅ 檢測到已存在 Google Drive 的模型，直接載入，跳過訓練！\")\n",
        "    model = tf.keras.models.load_model(MODEL_PATH)\n",
        "else:\n",
        "    print(\"⚠️ 尚未建立模型，開始訓練...\")\n",
        "    model = models.Sequential([\n",
        "        layers.Input(shape=(28, 28, 1)), # 修正警告：使用獨立的 Input 層\n",
        "        layers.Conv2D(32, (3, 3), activation='relu'),\n",
        "        layers.BatchNormalization(),\n",
        "        layers.MaxPooling2D((2, 2)),\n",
        "        layers.Conv2D(64, (3, 3), activation='relu'),\n",
        "        layers.BatchNormalization(),\n",
        "        layers.MaxPooling2D((2, 2)),\n",
        "        layers.Flatten(),\n",
        "        layers.Dense(128, activation='relu'),\n",
        "        layers.Dense(10, activation='softmax')\n",
        "    ])\n",
        "\n",
        "    model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])\n",
        "\n",
        "    # 訓練模型 (若有開啟 GPU，這裡會飛快)\n",
        "    history = model.fit(x_train, y_train, epochs=5, batch_size=128,\n",
        "                        validation_data=(x_test, y_test), verbose=1)\n",
        "\n",
        "    print(\"\\n💾 正在將訓練好的模型儲存到 Google Drive...\")\n",
        "    model.save(MODEL_PATH)\n",
        "    print(f\"✅ 模型已安全儲存至：{MODEL_PATH}\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "MX_Jw3nYIKrz",
        "outputId": "2ac57769-b951-4256-804b-e392f87e8323"
      },
      "execution_count": 2,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "⏳ 步驟 2: 模型建構與訓練...\n",
            "\n",
            "✅ 檢測到已存在 Google Drive 的模型，直接載入，跳過訓練！\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# 儲存格 3：產生訓練分析報告與驗證數據\n",
        "\n",
        "1.正確率\n",
        "\n",
        "2.召回率\n",
        "\n",
        "3.f1參數"
      ],
      "metadata": {
        "id": "I2qS_xn9I8k0"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "print(\"⏳ 步驟 3: 產生訓練分析報告與驗證數據...\\n\")\n",
        "\n",
        "# 繪製曲線 (只有剛剛訓練完才會有 history 變數)\n",
        "if 'history' in locals():\n",
        "    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n",
        "\n",
        "    ax1.plot(history.history['accuracy'], label='Training Accuracy', color='#1a73e8', marker='o')\n",
        "    ax1.plot(history.history['val_accuracy'], label='Validation Accuracy', color='#34a853', marker='x')\n",
        "    ax1.set_title('Model Accuracy Curve', fontsize=14, fontweight='bold')\n",
        "    ax1.set_xlabel('Epochs')\n",
        "    ax1.set_ylabel('Accuracy')\n",
        "    ax1.legend()\n",
        "    ax1.grid(True, linestyle='--')\n",
        "\n",
        "    ax2.plot(history.history['loss'], label='Training Loss', color='#d93025', marker='o')\n",
        "    ax2.plot(history.history['val_loss'], label='Validation Loss', color='#fbbc04', marker='x')\n",
        "    ax2.set_title('Model Loss Curve', fontsize=14, fontweight='bold')\n",
        "    ax2.set_xlabel('Epochs')\n",
        "    ax2.set_ylabel('Loss')\n",
        "    ax2.legend()\n",
        "    ax2.grid(True, linestyle='--')\n",
        "\n",
        "    plt.show()\n",
        "else:\n",
        "    print(\"💡 提示：模型為直接從 Google Drive 載入，本次未重新訓練，無歷史軌跡曲線。\")\n",
        "\n",
        "print(\"\\n\" + \"=\"*60 + \"\\n 正在計算測試集 0-9 每個數字的詳細分析指標... \\n\" + \"=\"*60)\n",
        "\n",
        "# 預測並計算指標\n",
        "predictions = model.predict(x_test, batch_size=256, verbose=0)\n",
        "y_pred = np.argmax(predictions, axis=1)\n",
        "report_dict = classification_report(y_test, y_pred, output_dict=True)\n",
        "\n",
        "analysis_data = []\n",
        "for i in range(10):\n",
        "    label = str(i)\n",
        "    analysis_data.append({\n",
        "        \"數字\": i,\n",
        "        \"精確率 (Precision)\": f\"{report_dict[label]['precision']*100:.2f}%\",\n",
        "        \"召回率 (Recall)\": f\"{report_dict[label]['recall']*100:.2f}%\",\n",
        "        \"F1-分數 (F1-Score)\": f\"{report_dict[label]['f1-score']*100:.2f}%\",\n",
        "        \"測試樣本總數\": int(report_dict[label]['support'])\n",
        "    })\n",
        "\n",
        "df_analysis = pd.DataFrame(analysis_data)\n",
        "display(HTML(\"<h3>📊 每個數字的分類效能詳細分析表</h3>\"))\n",
        "display(df_analysis)\n",
        "\n",
        "print(f\"\\n✅ 總體測試集平均準確率 (Overall Accuracy): {report_dict['accuracy']*100:.2f}%\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 559
        },
        "id": "oUuMcqlZIPd9",
        "outputId": "ec65b42e-839b-4381-a1f4-68ce94caeb36"
      },
      "execution_count": 3,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "⏳ 步驟 3: 產生訓練分析報告與驗證數據...\n",
            "\n",
            "💡 提示：模型為直接從 Google Drive 載入，本次未重新訓練，無歷史軌跡曲線。\n",
            "\n",
            "============================================================\n",
            " 正在計算測試集 0-9 每個數字的詳細分析指標... \n",
            "============================================================\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.HTML object>"
            ],
            "text/html": [
              "<h3>📊 每個數字的分類效能詳細分析表</h3>"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "   數字 精確率 (Precision) 召回率 (Recall) F1-分數 (F1-Score)  測試樣本總數\n",
              "0   0          95.40%       95.44%           95.42%    2630\n",
              "1   1          95.57%       95.73%           95.65%    4171\n",
              "2   2          94.60%       95.89%           95.24%    3580\n",
              "3   3          91.12%       93.72%           92.40%    3088\n",
              "4   4          96.93%       94.77%           95.84%    3000\n",
              "5   5          96.96%       92.54%           94.70%    2789\n",
              "6   6          93.35%       95.24%           94.28%    2710\n",
              "7   7          97.36%       94.55%           95.93%    2842\n",
              "8   8          95.40%       92.04%           93.69%    2589\n",
              "9   9          89.88%       95.66%           92.68%    2601"
            ],
            "text/html": [
              "\n",
              "  <div id=\"df-be208c59-c357-4d89-8dbc-caf2ea2a6836\" class=\"colab-df-container\">\n",
              "    <div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>數字</th>\n",
              "      <th>精確率 (Precision)</th>\n",
              "      <th>召回率 (Recall)</th>\n",
              "      <th>F1-分數 (F1-Score)</th>\n",
              "      <th>測試樣本總數</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>0</td>\n",
              "      <td>95.40%</td>\n",
              "      <td>95.44%</td>\n",
              "      <td>95.42%</td>\n",
              "      <td>2630</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>1</td>\n",
              "      <td>95.57%</td>\n",
              "      <td>95.73%</td>\n",
              "      <td>95.65%</td>\n",
              "      <td>4171</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>2</td>\n",
              "      <td>94.60%</td>\n",
              "      <td>95.89%</td>\n",
              "      <td>95.24%</td>\n",
              "      <td>3580</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>3</td>\n",
              "      <td>91.12%</td>\n",
              "      <td>93.72%</td>\n",
              "      <td>92.40%</td>\n",
              "      <td>3088</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>4</td>\n",
              "      <td>96.93%</td>\n",
              "      <td>94.77%</td>\n",
              "      <td>95.84%</td>\n",
              "      <td>3000</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>5</th>\n",
              "      <td>5</td>\n",
              "      <td>96.96%</td>\n",
              "      <td>92.54%</td>\n",
              "      <td>94.70%</td>\n",
              "      <td>2789</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>6</th>\n",
              "      <td>6</td>\n",
              "      <td>93.35%</td>\n",
              "      <td>95.24%</td>\n",
              "      <td>94.28%</td>\n",
              "      <td>2710</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>7</th>\n",
              "      <td>7</td>\n",
              "      <td>97.36%</td>\n",
              "      <td>94.55%</td>\n",
              "      <td>95.93%</td>\n",
              "      <td>2842</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>8</th>\n",
              "      <td>8</td>\n",
              "      <td>95.40%</td>\n",
              "      <td>92.04%</td>\n",
              "      <td>93.69%</td>\n",
              "      <td>2589</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>9</th>\n",
              "      <td>9</td>\n",
              "      <td>89.88%</td>\n",
              "      <td>95.66%</td>\n",
              "      <td>92.68%</td>\n",
              "      <td>2601</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>\n",
              "    <div class=\"colab-df-buttons\">\n",
              "\n",
              "  <div class=\"colab-df-container\">\n",
              "    <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-be208c59-c357-4d89-8dbc-caf2ea2a6836')\"\n",
              "            title=\"Convert this dataframe to an interactive table.\"\n",
              "            style=\"display:none;\">\n",
              "\n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
              "    <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
              "  </svg>\n",
              "    </button>\n",
              "\n",
              "  <style>\n",
              "    .colab-df-container {\n",
              "      display:flex;\n",
              "      gap: 12px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert {\n",
              "      background-color: #E8F0FE;\n",
              "      border: none;\n",
              "      border-radius: 50%;\n",
              "      cursor: pointer;\n",
              "      display: none;\n",
              "      fill: #1967D2;\n",
              "      height: 32px;\n",
              "      padding: 0 0 0 0;\n",
              "      width: 32px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert:hover {\n",
              "      background-color: #E2EBFA;\n",
              "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "      fill: #174EA6;\n",
              "    }\n",
              "\n",
              "    .colab-df-buttons div {\n",
              "      margin-bottom: 4px;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert {\n",
              "      background-color: #3B4455;\n",
              "      fill: #D2E3FC;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert:hover {\n",
              "      background-color: #434B5C;\n",
              "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "      fill: #FFFFFF;\n",
              "    }\n",
              "  </style>\n",
              "\n",
              "    <script>\n",
              "      const buttonEl =\n",
              "        document.querySelector('#df-be208c59-c357-4d89-8dbc-caf2ea2a6836 button.colab-df-convert');\n",
              "      buttonEl.style.display =\n",
              "        google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "      async function convertToInteractive(key) {\n",
              "        const element = document.querySelector('#df-be208c59-c357-4d89-8dbc-caf2ea2a6836');\n",
              "        const dataTable =\n",
              "          await google.colab.kernel.invokeFunction('convertToInteractive',\n",
              "                                                    [key], {});\n",
              "        if (!dataTable) return;\n",
              "\n",
              "        const docLinkHtml = 'Like what you see? Visit the ' +\n",
              "          '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
              "          + ' to learn more about interactive tables.';\n",
              "        element.innerHTML = '';\n",
              "        dataTable['output_type'] = 'display_data';\n",
              "        await google.colab.output.renderOutput(dataTable, element);\n",
              "        const docLink = document.createElement('div');\n",
              "        docLink.innerHTML = docLinkHtml;\n",
              "        element.appendChild(docLink);\n",
              "      }\n",
              "    </script>\n",
              "  </div>\n",
              "\n",
              "\n",
              "  <div id=\"id_b9ae4a96-9dbc-4dca-9939-2465c539b90e\">\n",
              "    <style>\n",
              "      .colab-df-generate {\n",
              "        background-color: #E8F0FE;\n",
              "        border: none;\n",
              "        border-radius: 50%;\n",
              "        cursor: pointer;\n",
              "        display: none;\n",
              "        fill: #1967D2;\n",
              "        height: 32px;\n",
              "        padding: 0 0 0 0;\n",
              "        width: 32px;\n",
              "      }\n",
              "\n",
              "      .colab-df-generate:hover {\n",
              "        background-color: #E2EBFA;\n",
              "        box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "        fill: #174EA6;\n",
              "      }\n",
              "\n",
              "      [theme=dark] .colab-df-generate {\n",
              "        background-color: #3B4455;\n",
              "        fill: #D2E3FC;\n",
              "      }\n",
              "\n",
              "      [theme=dark] .colab-df-generate:hover {\n",
              "        background-color: #434B5C;\n",
              "        box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "        filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "        fill: #FFFFFF;\n",
              "      }\n",
              "    </style>\n",
              "    <button class=\"colab-df-generate\" onclick=\"generateWithVariable('df_analysis')\"\n",
              "            title=\"Generate code using this dataframe.\"\n",
              "            style=\"display:none;\">\n",
              "\n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
              "       width=\"24px\">\n",
              "    <path d=\"M7,19H8.4L18.45,9,17,7.55,7,17.6ZM5,21V16.75L18.45,3.32a2,2,0,0,1,2.83,0l1.4,1.43a1.91,1.91,0,0,1,.58,1.4,1.91,1.91,0,0,1-.58,1.4L9.25,21ZM18.45,9,17,7.55Zm-12,3A5.31,5.31,0,0,0,4.9,8.1,5.31,5.31,0,0,0,1,6.5,5.31,5.31,0,0,0,4.9,4.9,5.31,5.31,0,0,0,6.5,1,5.31,5.31,0,0,0,8.1,4.9,5.31,5.31,0,0,0,12,6.5,5.46,5.46,0,0,0,6.5,12Z\"/>\n",
              "  </svg>\n",
              "    </button>\n",
              "    <script>\n",
              "      (() => {\n",
              "      const buttonEl =\n",
              "        document.querySelector('#id_b9ae4a96-9dbc-4dca-9939-2465c539b90e button.colab-df-generate');\n",
              "      buttonEl.style.display =\n",
              "        google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "      buttonEl.onclick = () => {\n",
              "        google.colab.notebook.generateWithVariable('df_analysis');\n",
              "      }\n",
              "      })();\n",
              "    </script>\n",
              "  </div>\n",
              "\n",
              "    </div>\n",
              "  </div>\n"
            ],
            "application/vnd.google.colaboratory.intrinsic+json": {
              "type": "dataframe",
              "variable_name": "df_analysis",
              "summary": "{\n  \"name\": \"df_analysis\",\n  \"rows\": 10,\n  \"fields\": [\n    {\n      \"column\": \"\\u6578\\u5b57\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 3,\n        \"min\": 0,\n        \"max\": 9,\n        \"num_unique_values\": 10,\n        \"samples\": [\n          8,\n          1,\n          5\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"\\u7cbe\\u78ba\\u7387 (Precision)\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 9,\n        \"samples\": [\n          \"97.36%\",\n          \"95.57%\",\n          \"96.96%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"\\u53ec\\u56de\\u7387 (Recall)\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 10,\n        \"samples\": [\n          \"92.04%\",\n          \"95.73%\",\n          \"92.54%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"F1-\\u5206\\u6578 (F1-Score)\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 10,\n        \"samples\": [\n          \"93.69%\",\n          \"95.65%\",\n          \"94.70%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"\\u6e2c\\u8a66\\u6a23\\u672c\\u7e3d\\u6578\",\n      \"properties\": {\n        \"dtype\": \"number\",\n        \"std\": 509,\n        \"min\": 2589,\n        \"max\": 4171,\n        \"num_unique_values\": 10,\n        \"samples\": [\n          2589,\n          4171,\n          2789\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    }\n  ]\n}"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "✅ 總體測試集平均準確率 (Overall Accuracy): 94.64%\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "儲存格4 安裝gradio"
      ],
      "metadata": {
        "id": "PYEyZ90Ielag"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "!pip install gradio"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "CyXVVo6ae1-f",
        "outputId": "d169e3c0-22c2-4c81-bc66-180f820095da"
      },
      "execution_count": 4,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Requirement already satisfied: gradio in /usr/local/lib/python3.12/dist-packages (5.50.0)\n",
            "Requirement already satisfied: aiofiles<25.0,>=22.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (24.1.0)\n",
            "Requirement already satisfied: anyio<5.0,>=3.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (4.13.0)\n",
            "Requirement already satisfied: brotli>=1.1.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (1.2.0)\n",
            "Requirement already satisfied: fastapi<1.0,>=0.115.2 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.136.1)\n",
            "Requirement already satisfied: ffmpy in /usr/local/lib/python3.12/dist-packages (from gradio) (1.0.0)\n",
            "Requirement already satisfied: gradio-client==1.14.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (1.14.0)\n",
            "Requirement already satisfied: groovy~=0.1 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.1.2)\n",
            "Requirement already satisfied: httpx<1.0,>=0.24.1 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.28.1)\n",
            "Requirement already satisfied: huggingface-hub<2.0,>=0.33.5 in /usr/local/lib/python3.12/dist-packages (from gradio) (1.16.1)\n",
            "Requirement already satisfied: jinja2<4.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (3.1.6)\n",
            "Requirement already satisfied: markupsafe<4.0,>=2.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (3.0.3)\n",
            "Requirement already satisfied: numpy<3.0,>=1.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (2.0.2)\n",
            "Requirement already satisfied: orjson~=3.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (3.11.9)\n",
            "Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from gradio) (26.2)\n",
            "Requirement already satisfied: pandas<3.0,>=1.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (2.2.2)\n",
            "Requirement already satisfied: pillow<12.0,>=8.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (11.3.0)\n",
            "Requirement already satisfied: pydantic<=2.12.3,>=2.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (2.12.3)\n",
            "Requirement already satisfied: pydub in /usr/local/lib/python3.12/dist-packages (from gradio) (0.25.1)\n",
            "Requirement already satisfied: python-multipart>=0.0.18 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.0.29)\n",
            "Requirement already satisfied: pyyaml<7.0,>=5.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (6.0.3)\n",
            "Requirement already satisfied: ruff>=0.9.3 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.15.14)\n",
            "Requirement already satisfied: safehttpx<0.2.0,>=0.1.6 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.1.7)\n",
            "Requirement already satisfied: semantic-version~=2.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (2.10.0)\n",
            "Requirement already satisfied: starlette<1.0,>=0.40.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.52.1)\n",
            "Requirement already satisfied: tomlkit<0.14.0,>=0.12.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.13.3)\n",
            "Requirement already satisfied: typer<1.0,>=0.12 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.25.1)\n",
            "Requirement already satisfied: typing-extensions~=4.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (4.15.0)\n",
            "Requirement already satisfied: uvicorn>=0.14.0 in /usr/local/lib/python3.12/dist-packages (from gradio) (0.47.0)\n",
            "Requirement already satisfied: fsspec in /usr/local/lib/python3.12/dist-packages (from gradio-client==1.14.0->gradio) (2025.3.0)\n",
            "Requirement already satisfied: websockets<16.0,>=13.0 in /usr/local/lib/python3.12/dist-packages (from gradio-client==1.14.0->gradio) (15.0.1)\n",
            "Requirement already satisfied: idna>=2.8 in /usr/local/lib/python3.12/dist-packages (from anyio<5.0,>=3.0->gradio) (3.15)\n",
            "Requirement already satisfied: typing-inspection>=0.4.2 in /usr/local/lib/python3.12/dist-packages (from fastapi<1.0,>=0.115.2->gradio) (0.4.2)\n",
            "Requirement already satisfied: annotated-doc>=0.0.2 in /usr/local/lib/python3.12/dist-packages (from fastapi<1.0,>=0.115.2->gradio) (0.0.4)\n",
            "Requirement already satisfied: certifi in /usr/local/lib/python3.12/dist-packages (from httpx<1.0,>=0.24.1->gradio) (2026.5.20)\n",
            "Requirement already satisfied: httpcore==1.* in /usr/local/lib/python3.12/dist-packages (from httpx<1.0,>=0.24.1->gradio) (1.0.9)\n",
            "Requirement already satisfied: h11>=0.16 in /usr/local/lib/python3.12/dist-packages (from httpcore==1.*->httpx<1.0,>=0.24.1->gradio) (0.16.0)\n",
            "Requirement already satisfied: filelock>=3.10.0 in /usr/local/lib/python3.12/dist-packages (from huggingface-hub<2.0,>=0.33.5->gradio) (3.29.0)\n",
            "Requirement already satisfied: hf-xet<2.0.0,>=1.4.3 in /usr/local/lib/python3.12/dist-packages (from huggingface-hub<2.0,>=0.33.5->gradio) (1.5.0)\n",
            "Requirement already satisfied: tqdm>=4.42.1 in /usr/local/lib/python3.12/dist-packages (from huggingface-hub<2.0,>=0.33.5->gradio) (4.67.3)\n",
            "Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas<3.0,>=1.0->gradio) (2.9.0.post0)\n",
            "Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas<3.0,>=1.0->gradio) (2025.2)\n",
            "Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas<3.0,>=1.0->gradio) (2026.2)\n",
            "Requirement already satisfied: annotated-types>=0.6.0 in /usr/local/lib/python3.12/dist-packages (from pydantic<=2.12.3,>=2.0->gradio) (0.7.0)\n",
            "Requirement already satisfied: pydantic-core==2.41.4 in /usr/local/lib/python3.12/dist-packages (from pydantic<=2.12.3,>=2.0->gradio) (2.41.4)\n",
            "Requirement already satisfied: click>=8.2.1 in /usr/local/lib/python3.12/dist-packages (from typer<1.0,>=0.12->gradio) (8.4.0)\n",
            "Requirement already satisfied: shellingham>=1.3.0 in /usr/local/lib/python3.12/dist-packages (from typer<1.0,>=0.12->gradio) (1.5.4)\n",
            "Requirement already satisfied: rich>=13.8.0 in /usr/local/lib/python3.12/dist-packages (from typer<1.0,>=0.12->gradio) (13.9.4)\n",
            "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas<3.0,>=1.0->gradio) (1.17.0)\n",
            "Requirement already satisfied: markdown-it-py>=2.2.0 in /usr/local/lib/python3.12/dist-packages (from rich>=13.8.0->typer<1.0,>=0.12->gradio) (4.2.0)\n",
            "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.12/dist-packages (from rich>=13.8.0->typer<1.0,>=0.12->gradio) (2.20.0)\n",
            "Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.12/dist-packages (from markdown-it-py>=2.2.0->rich>=13.8.0->typer<1.0,>=0.12->gradio) (0.1.2)\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# 儲存格 5：啟動手寫圖片上傳辨識\n"
      ],
      "metadata": {
        "id": "0ks_elSvJFAc"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import gradio as gr\n",
        "from PIL import Image, ImageOps\n",
        "import numpy as np\n",
        "\n",
        "print(\"⏳ 步驟 4: 啟動手寫圖片上傳辨識介面 (Gradio)...\\\\n\")\n",
        "\n",
        "def process_image(img):\n",
        "    \"\"\"處理圖片並回傳模型預測結果給 Gradio\"\"\"\n",
        "    if img is None:\n",
        "        return {\"請上傳圖片\": 0.0}\n",
        "\n",
        "    # 確保圖片為灰階\n",
        "    img = img.convert('L')\n",
        "\n",
        "    # 顏色反轉 (原本為白底黑字 -> 黑底白字，符合模型訓練特徵)\n",
        "    img_inv = ImageOps.invert(img)\n",
        "\n",
        "    # 取得數字邊界框，去除多餘空白\n",
        "    bbox = img_inv.getbbox()\n",
        "\n",
        "    if bbox:\n",
        "        # 裁切出數字部分\n",
        "        img_final = img_inv.crop(bbox)\n",
        "        # 增加邊框並縮放至 28x28\n",
        "        img_final = ImageOps.expand(img_final, border=4, fill=0).resize((28, 28), Image.Resampling.LANCZOS)\n",
        "        # 轉為模型需要的 Numpy 陣列並標準化\n",
        "        img_array = np.array(img_final).reshape(1, 28, 28, 1) / 255.0\n",
        "\n",
        "        # 模型預測\n",
        "        res = model.predict(img_array, verbose=0)[0]\n",
        "\n",
        "        # 回傳字典格式 {數字: 機率}，Gradio 會自動繪製出漂亮的信心度長條圖\n",
        "        return {str(i): float(res[i]) for i in range(10)}\n",
        "    else:\n",
        "        return {\"圖片過於空白或無法辨識\": 0.0}\n",
        "\n",
        "# 建立 Gradio 網頁介面\n",
        "ui = gr.Interface(\n",
        "    fn=process_image,\n",
        "    inputs=gr.Image(type=\"pil\", image_mode=\"L\", label=\"上傳手寫圖片 (建議白底黑字)\"),\n",
        "    outputs=gr.Label(num_top_classes=3, label=\"🤖 AI 預測結果 (Top 3)\"),\n",
        "    title=\"🎨 智光商工電機電子群 數字辨識 AI 網頁版\",\n",
        "    description=\"請點擊下方虛線框上傳圖片，或是直接將圖片拖曳進來。AI 將立刻分析數字並給出信心度評估！\",\n",
        "    theme=gr.themes.Soft() # 使用柔和的現代化主題\n",
        ")\n",
        "\n",
        "# 啟動介面\n",
        "# share=True 會自動生成一個 public URL (例如 https://xxxx.gradio.live)，72小時內有效\n",
        "ui.launch(share=True, debug=False)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 628
        },
        "id": "A-2eGLRxe9L3",
        "outputId": "8502b2ff-aaab-4c6d-e854-396da73f5ac9"
      },
      "execution_count": 5,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "⏳ 步驟 4: 啟動手寫圖片上傳辨識介面 (Gradio)...\\n\n",
            "Colab notebook detected. To show errors in colab notebook, set debug=True in launch()\n",
            "* Running on public URL: https://1bfa487bb9dee4d6bd.gradio.live\n",
            "\n",
            "This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.HTML object>"
            ],
            "text/html": [
              "<div><iframe src=\"https://1bfa487bb9dee4d6bd.gradio.live\" width=\"100%\" height=\"500\" allow=\"autoplay; camera; microphone; clipboard-read; clipboard-write;\" frameborder=\"0\" allowfullscreen></iframe></div>"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": []
          },
          "metadata": {},
          "execution_count": 5
        }
      ]
    }
  ]
}