3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

個人開発アプリにカメラで文字を読み取るOCR機能をつけた

3
Posted at

はじめに

個人開発のアプリでパンの消費期限をカメラで読み取りたいと考えOCR機能の実装にチャレンジしました。

開発環境
Ruby 3.2.2
Rails 7.1.6
デプロイ:Render

OCRとは

OCRとはOptical Character Recognition(またはReader)の略。
日本語では「光学的文字認識」と訳されます。
入力、手書き、印刷されたテキストを画像からマシンでエンコードされたテキストに変換する基本的な技術のことです。

OCRには Google Cloud Vision API を採用しました。
理由は、日本語OCRの精度が高く、APIとしてすぐ利用でき、短期間で安定した機能実装ができると判断したためです。

Cloud Vision APIによるOCR機能の実装

やりたいこと

写真を撮って画像から消費期限を抽出して自動で入力させたい

流れ

撮影した画像をPOST
   ↓ ①受け取る(Controller)
Google Vision API で画像内の文字を抽出
   ↓ ②文字を読む(Service)
ノイズだらけのテキストから「日付だけ」を取り出す
   ↓ ③日付を抜き出す(Service)
日付をJSONで返し、入力欄に自動セット

実装内容

CloudVision導入

gemをインストール

gem "google-cloud-vision"

Google Cloud Consoleからダウンロードしたサービスアカウントの JSON鍵ファイル を、Railsプロジェクト内に配置します。(config/google-vision.json)

注意!鍵なのでGitHubに上げないこと
.gitignoreに追加して除外します。

鍵の渡し方(ローカルと本番で違う)

ここが少し躓きました。
本番環境(Render)では、Gitに含めていない秘密鍵ファイルは存在しません。

そのため、JSONの中身を環境変数に保存し、起動時に一時ファイルとして書き出して利用しています。

ローカル(Docker) ファイルがあるので、その場所を環境変数で指定します。

compose.yml
environment:
    GOOGLE_APPLICATION_CREDENTIALS: /app/config/google-vision.json

本番(Render) ファイルが置けないので、JSONの中身そのものを環境変数に入れておき、起動時に一時ファイルへ書き出して使います。
GOOGLE_CREDENTIALS_JSON=(JSON鍵の中身をまるごと貼り付け)

controller

画像を受け取りOcrServiceに渡します。
expiration&.strftime("%Y-%m-%d")は、
日付データを「年(4桁)-月(2桁)-日(2桁)」という文字列に並び替え、もし nil だったら、右側の命令(strftime)は無視して、そのまま nil を返す という意味です。

ocrs_controller.rb
class OcrsController < ApplicationController
  skip_before_action :verify_authenticity_token, only: [:create]

  def create
    image = params[:image]
    return render json: { error: "画像がありません" }, status: :bad_request if image.nil?

    expiration = OcrService.extract_expiration(image.tempfile.path)

    render json: { expiration: expiration&.strftime("%Y-%m-%d") }
  end
end

OcrServiceクラスを作成し実装

services/ocr_service.rbを作成します。
ここにOCR結果をどう処理するかのロジックを書いていきます。

まず画像を渡し文字に変換するよう依頼します。

ocr_service.rb
response = vision.text_detection image: image_path

これで画像に写っている文字を読み取り返してくれます。
画像
Image from Gyazo
読み取り結果
Image from Gyazo

おもしろい笑
文字は返ってきましたが、
完璧に読み取るのは難しいみたいです。
このパターンは肝心な消費期限の日付が読み取れておらず、この時点で失敗です:cry:

今回は読み取れた前提で、次に進みます。

キーになるのは「消費期限」というテキストです。
その周辺に日付があればそれが消費期限である可能性が高いからです。
一応「消費」「賞味」という文字があればその行を優先してみるようにしました。

それから日付の表記は上の写真のように26.4.1のような表記か2026.4.1のような表記なので正規表現を使って抜き出します。
これもパンによっては微妙に表記が違うので対応できるようにするのが苦労しました。

ocr_service.rb
#リクエスト部分省略してます

   #消費期限っぽいものを選ぶ(候補から確定する)
   def self.extract_best_date(text)
    lines = text.split("\n")
    # 消費期限がある行を優先
    target_line = lines.find { |line| line.match?(/消費|賞味/) }
    # なければ全体
    target_line ||= text
    # 余計な空白を削除
    target_line = target_line.gsub(/\s+/, "")
  
    dates = extract_dates(target_line)
    parsed_dates = dates.map { |d| normalize_date(d) }.compact
    return nil if parsed_dates.empty? #エラー回避、該当なしの場合は読み取り終了
    # 消費期限は基本「これから来る日付」なので、今日以降を優先する
    future_dates = parsed_dates.select { |d| d >= Time.zone.today }
    return future_dates.min if future_dates.any?
  
    parsed_dates.min  # 未来日がなければ一番近い日付
  end

  #日付っぽい数字を探す(消費期限候補)
  def self.extract_dates(text)
    patterns = [
      /\d{2}\.\d{1,2}\.\d{1,2}/,   #2桁と1桁両方に対応できるよう修正
      /\d{4}[\/\.\-]\d{1,2}[\/\.\-]\d{1,2}/
    ]
    patterns.flat_map { |p| text.scan(p) }
  end

抜き出した日付は、まだただの「文字(テキスト)」です。これを「日付」として扱える特別なデータ(Date型)に変換します。

ocr_service.rb
  def self.normalize_date(date_str)
    if date_str =~ /^\d{2}\.\d{2}\.\d{2}$/
      Date.strptime(date_str, "%y.%m.%d")
    else
      Date.parse(date_str)
    end
  rescue ArgumentError
    nil
  end

まとめ

CloudVisionAPI を使用してOCR機能を実装することができました!
APIの導入で、画像からテキストが返ってきて感動しました。
ただパンの種類によっては読み取りに時間がかかったり、エラーになったりしてしまいます。
精度としてはまだまだ改良の余地があるので色々試してみて改良していきます!

参考記事

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?