iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
自我挑戰組

攜手 AI 從零開始打造一款 Flutter 應用程式系列 第 16

Day 16: 讓 App 開口說話 - 整合 ML Kit 實現圖片文字辨識

  • 分享至 

  • xImage
  •  

前言

大家好!在昨天的進度中,我們的「省錢拍拍」App 成功地睜開了眼睛,能夠透過 image_picker 捕捉來自真實世界的發票圖片。但對 App 來說,這張圖片目前還只是一堆無意義的像素點。

今天,我們要賦予 App 一項超能力:閱讀。我們將深入探索 Google 強大的機器學習框架 ML Kit,實作光學字元辨識 (OCR) 功能。目標是將昨天拍下的那張圖片,轉換成 App 可以理解、可以處理的純文字資料,真正地化像素為價值!

Step 1: 加入 ML Kit 套件與平台依賴

首先,我們需要加入 Flutter 套件和 Android 平台的原生依賴。

  1. Flutter 套件: google_mlkit_text_recognition
    執行以下指令,它會自動抓取最新穩定版並加入 pubspec.yaml。:
flutter pub add google_mlkit_text_recognition
  1. Android 原生依賴 (關鍵)
    為了讓中文辨識模型能直接打包進 App,優化首次使用體驗,我們需要手動在 Android 設定中加入依賴。

打開 android/app/build.gradle 檔案,在 dependencies { ... } 區塊中,加入中文辨識模型:

// android/app/build.gradle
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

    // ML Kit 文字辨識(中文)- 加上這行
    implementation 'com.google.mlkit:text-recognition-chinese:16.0.0'
}

Step 2: 平台相關設定與專案清理 (重要)

因為我們同時修改了 Flutter 的 pubspec.yaml 和 Android 的原生設定 build.gradle,為了確保所有變更都正確生效,最好執行一次完整的清理與重建。

在專案根目錄終端機中,依序執行以下指令:

flutter clean
flutter pub get
cd android
./gradlew clean  # 在 Windows 上是 gradlew.bat clean
cd ..
flutter run

Step 3: 建立一個高效的文字辨識服務

遵循我們在 Day 13 建立的良好習慣,並採納性能優化的建議,我們將 OCR 邏輯封裝在一個獨立的 Service 中。

lib/services 資料夾下建立 ocr_service.dart

// lib/services/ocr_service.dart
import 'dart.io';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';

class OcrService {
  // 共用一個 recognizer,減少重複載入模型與釋放的開銷
  late final TextRecognizer _recognizer =
      TextRecognizer(script: TextRecognitionScript.chinese);
  bool _closed = false;

  Future<String> processImage(File imageFile) async {
    try {
      final inputImage = InputImage.fromFilePath(imageFile.path);
      final RecognizedText recognizedText =
          await _recognizer.processImage(inputImage);
      return recognizedText.text;
    } catch (e) {
      // 這裡只會捕捉 Dart 層例外;原生層崩潰需看裝置日誌
      return '無法辨識圖片中的文字,請再試一次。錯誤:($e)';
    }
  }

  // 提供 dispose 方法,讓外部可以在適當時機釋放資源
  Future<void> dispose() async {
    if (!_closed) {
      await _recognizer.close();
      _closed = true;
    }
  }
}

Step 4: 在 ScanPage 中整合 OCR 功能

現在,我們回到 lib/scan_page.dart,用更穩健的方式來處理圖片選擇、狀態管理和錯誤捕捉。

以下是 lib/scan_page.dart 檔案完整程式碼。

// lib/scan_page.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:snapsaver/services/ocr_service.dart';

class ScanPage extends StatefulWidget {
  const ScanPage({super.key});
  @override
  State<ScanPage> createState() => _ScanPageState();
}

class _ScanPageState extends State<ScanPage> {
  File? _imageFile;
  final ImagePicker _picker = ImagePicker();
  final OcrService _ocrService = OcrService();

  bool _isProcessing = false;
  String? _recognizedText;

  Future<void> _pickImage(ImageSource source) async {
    // 限制圖片大小,降低 OOM (Out of Memory) 的機率
    final XFile? image = await _picker.pickImage(
      source: source,
      maxWidth: 1600,
      maxHeight: 1600,
      imageQuality: 85, // 適度壓縮圖片品質
    );
    if (image != null) {
      setState(() {
        _imageFile = File(image.path);
        _recognizedText = null; // 選擇新圖片後清空舊結果
      });
    }
  }
  
  Future<void> _processImage() async {
    if (_imageFile == null) return;

    setState(() {
      _isProcessing = true;
    });

    try {
      final text = await _ocrService.processImage(_imageFile!);
      if (!mounted) return;
      setState(() {
        _recognizedText = text;
      });
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('辨識失敗:$e')),
      );
    } finally {
      if (mounted) {
        setState(() {
          _isProcessing = false;
        });
      }
    }
  }

  @override
  void dispose() {
    _ocrService.dispose(); // 在頁面銷毀時釋放 recognizer
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('掃描發票')),
      body: Column(
        children: [
          Expanded(
            flex: 3,
            child: _imageFile == null
                ? const Center(child: Text('尚未選擇圖片'))
                : Container(
                    padding: const EdgeInsets.all(16.0),
                    child: Image.file(_imageFile!),
                  ),
          ),
          Expanded(
            flex: 2,
            child: _isProcessing
                ? const Center(child: CircularProgressIndicator())
                : SingleChildScrollView(
                    padding: const EdgeInsets.all(16.0),
                    child: Text(_recognizedText ?? '點擊右下角按鈕開始辨識...'),
                  ),
          ),
        ],
      ),
      bottomNavigationBar: BottomAppBar(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            IconButton(icon: const Icon(Icons.camera_alt), onPressed: () => _pickImage(ImageSource.camera)),
            IconButton(icon: const Icon(Icons.photo_library), onPressed: () => _pickImage(ImageSource.gallery)),
            IconButton(
              icon: const Icon(Icons.analytics),
              onPressed: (_imageFile != null && !_isProcessing) ? _processImage : null,
              tooltip: '開始辨識',
            ),
          ],
        ),
      ),
    );
  }
}

OCR掃完

今日結語

今天我們為「省錢拍拍」安裝了一個強大且高效的「閱讀器」!我們成功地:

  1. 整合了 Google ML Kit,並為 Android 平台預載了中文模型。
  2. 完成了必要的平台設定與專案清理。
  3. 建立了一個可重用且高效的 OcrService
  4. 以更穩健的方式,實現了從選取圖片到辨識出文字並顯示在畫面上的完整流程。

我們已經將一張靜態的圖片,轉化為了一段充滿潛力的純文字資料。雖然這些文字目前看起來還有些雜亂,但它已經是 AI 大腦可以處理的「食材」。

明天,我們將把這些食材交給我們的「大廚」——正式對接 Google Gemini API。我們將學習如何設計巧妙的「提問」(Prompt),讓 Gemini 從這段文字中,精準地為我們提取出金額、店家名稱等關鍵資訊!


上一篇
Day 15: 智慧掃描第一步 - 整合 image_picker 喚醒相機
下一篇
Day 17: App 的 AI 大腦上線 - 串接 Google Gemini API
系列文
攜手 AI 從零開始打造一款 Flutter 應用程式20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言