MAGAZINE

ルーターマガジン

AI/機械学習

Gemini Nanoで名刺読み取りを試してみた

2026.03.14
Pocket

はじめに

ChromeにはローカルLLMである Gemini Nano が内臓されています。

GeminiのRest APIを叩いて高性能なモデルを利用するのが簡単ですが、API料金がかかってしまいますし、
医療や個人情報に関するデータのように外部に送信したくないデータを扱う場合には、ローカルで動作するモデルが欲しくなります。

今回は、Prompt APIを使って名刺の画像を読み取り、情報を抽出するというタスクを試してみようと思います。

Gemini Nanoをブラウザのコンソールから触ってみる

Gemini Nanoの概要については、AI on Chrome というページから確認できます。今回使用するのはPrompt APIです。
まずは、ブラウザのコンソールから動かしてみましょう。

始める前に、chromeのflagsで以下の設定を有効にしておきます。

  • chrome://flags/#optimization-guide-on-device-model
  • chrome://flags/#prompt-api-for-gemini-nano
  • chrome://flags/#prompt-api-for-gemini-nano-multimodal-input

これらを有効にしてブラウザを再起動した後、開発者ツールのコンソールから操作を行います。

まずは、Prompt APIが有効になっているかを確認しましょう。下のコードを実行してみてください。


await LanguageModel.availability();

'downloadable' と出力されていればOKです。

それではモデルのダウンロードを行います。以下のコードをコピペして実行してください。

const session = await LanguageModel.create({
  monitor(m) {
    m.addEventListener('downloadprogress', (e) => {
      console.log(`Downloaded ${e.loaded * 100}%`);
    });
  },
});

ダウンロードが完了したら、モデルが利用可能になっているかを確認します。

await LanguageModel.availability();

'available'と出力されれば準備完了です。

それでは、早速名刺の画像を読み取ってみましょう。下にあるjsを順番にコンソールに貼り付ければ動作します。
まずはchromeで名刺の画像を開いてください。
Prompt APIに画像を渡す方法はいくつかあるのですが、今回はhtmlのimg要素を直接渡す方法を使います。

const img = document.querySelector('img');

Prompt APIでは出力形式をJsonSchemaで指定できるので、名刺の情報を格納するスキーマを定義します。

const businessCardSchema = {
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "title": { "type": "string" },
    "company": { "type": "string" },
    "address": { "type": "string" },
    "email": { "type": "string" }
  },
  "required": ["name", "title", "company", "address", "email"]
};

プロンプトは以下のようにしてみました。

const promptText = "Extract the information from this business card image.
  Return ONLY valid JSON in the following format.
  If a field is missing, use null.
  Do not include any explanation text.

  IMPORTANT:
  - Preserve the original language exactly as written on the card.
  - Do NOT translate.
  - Do NOT normalize or rewrite text.
  - Keep names, company names, and addresses exactly as they appear.
  - Maintain original characters (including Japanese, English, symbols, etc.).";

それでは、テキストと画像の両方を入力として渡せるようにセッションを作成します。

const session = await LanguageModel.create({
  expectedInputs: [{ type: "text" }, { type: "image" }],
});

最後に、テキストと画像を渡して実行します。

const result = await session.prompt([
  {
    role: "user",
    content: [
      {
        type: "text",
        value: promptText,
      },
      {
        type: "image",
        value: img,
      }
    ],
  }
], {
  responseConstraint: businessCardSchema
});

実行が完了すると、result変数に結果が格納されます。
ここまでで、下の画像のように結果が得られているはずです。

Gemini Nanoをターミナルから動かしたい

Gemini Nanoはブラウザのコンソールから動かすことができますが、繰り返し実行したり自動化したりするにはターミナルから動かせると便利です。
以下のようにChromeをコマンドラインから起動し、flagsの設定やモデルのダウンロードを済ませておいてください。

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir=.chrome

起動コマンドのオプションの --user-data-dir は、通常のChromeの設定に影響を与えないためのものです。任意のディレクトリを指定してください。
このディレクトリにモデルがダウンロードされます。flagsを設定した後で再起動が必要になると思いますが、その時も同じコマンドで起動してください。
モデルのダウンロードが済んだら一旦Chromeを終了します。

Chromeは remote-debugging-port オプションを使って外部から操作することができます。以下のように起動してください。

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir=.chrome --remote-debugging-port=9222

次に、好みのプログラミング言語でDevTools Protocolを使ってChromeに接続します。ここではRubyの chrome_remote というgemを使う例を示します。
ちなみにpythonの場合は PyChromeDevTools 、Node.jsの場合は chrome-remote-interface などが使えます。

require 'chrome_remote'

img_path = 'path/to/your/business_card_image.jpg'

client = ChromeRemote.client
client.send_cmd('Page.navigate', url: "file://#{File.expand_path(img_path)}")

result = client.send_cmd(
  'Runtime.evaluate',
  expression: <<~JS,
    (async() => {
      const img = document.querySelector('img');
      const businessCardSchema = {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "title": { "type": "string" },
          "company": { "type": "string" },
          "address": { "type": "string" },
          "email": { "type": "string" }
        },
        "required": ["name", "title", "company", "address", "email"]
      };
      const promptText = "名刺の写真から名前・役職・会社名・住所・メールアドレスを抽出し、指定された形式のJSONを生成してください";
      const session = await LanguageModel.create({
        expectedInputs: [{ type: "text" }, { type: "image" }],
      });
      const result = await session.prompt([
        {
          role: "user",
          content: [
            {
              type: "text",
              value: promptText,
            },
            {
              type: "image",
              value: img,
            }
          ],
        }
      ], {
        responseConstraint: businessCardSchema
      });

      return result;
    })();
  JS
  awaitPromise: true,
  returnByValue: true
)

puts result['result']['value']

試しにChromeを起動した状態で上のコードを実行してみてください。名刺画像から情報が抽出されているはずです。

Gemini2.5 Flashとの比較

ここまでで、Gemini Nanoの自動化ができるようになりました。ここからが本題です。名刺読み取りの精度がどの程度なのかを評価してみましょう。

まずは評価の際に必要となる名刺のサンプルが必要です。Nano Bananaに20個の名刺サンプルを作ってもらいました。
名刺作成のスクリプトは以下の通りです。ここで登場する人名や会社名はGeminiに考えてもらいました。

#!/bin/bash

mkdir -p images

CARDS=(
  "佐藤 健一 | シニアソフトウェアエンジニア | テクノロジー・ソリューションズ株式会社 | 東京都港区六本木 6-10-1 | k.sato@techsol.jp"
  "鈴木 美咲 | グラフィックデザイナー | デザイン・スタジオ・ビジョン | 東京都渋谷区神宮前 4-12-10 | m.suzuki@designvision.com"
  "高橋 浩司 | プロジェクトマネージャー | グローバル・マネジメント株式会社 | 東京都千代田区丸の内 1-5-1 | k.takahashi@globalmgmt.co.jp"
  "田中 結衣 | マーケティングスペシャリスト | 未来マーケティング研究所 | 東京都新宿区西新宿 2-8-1 | y.tanaka@miraimkt.net"
  "伊藤 誠 | 営業部長 | 日本商事パートナーズ | 東京都中央区銀座 4-6-16 | m.ito@nihonshoji.com"
  "渡辺 陽子 | コンサルタント | エクセレント・コンサルティング | 東京都港区虎ノ門 1-23-1 | y.watanabe@ex-consulting.jp"
  "山本 裕太 | データサイエンティスト | データ・インサイト株式会社 | 東京都江東区豊洲 2-4-9 | y.yamamoto@datainsight.co.jp"
  "中村 杏奈 | 広報担当 | 広報戦略エージェンシー | 東京都渋谷区恵比寿 4-20-3 | a.nakamura@prstrategy.ag"
  "小林 茂 | 財務アドバイザー | ファイナンシャル・トラスト | 東京都千代田区大手町 1-1-1 | s.kobayashi@fintrust.jp"
  "加藤 瑠美 | UXデザイナー | クリエイティブ・UX・ラボ | 東京都目黒区上目黒 1-22-10 | r.kato@creativeux.lab"
  "吉田 剛 | 技術顧問 | テック・アドバイザリー | 東京都世田谷区玉川 3-17-1 | t.yoshida@techadv.co.jp"
  "山田 恵 | イベントプランナー | イベント・マジック | 東京都武蔵野市吉祥寺本町 1-1-1 | m.yamada@eventmagic.jp"
  "佐々木 翼 | システムアーキテクト | クラウド・アーキテクツ | 東京都港区赤坂 9-7-1 | t.sasaki@cloudarch.jp"
  "山口 香織 | 人事マネージャー | ヒューマン・リソース・ジャパン | 東京都渋谷区道玄坂 1-2-3 | k.yamaguchi@hrjapan.com"
  "松本 孝介 | 経営企画 | ストラテジー・プランニング | 東京都千代田区麹町 3-3-3 | k.matsumoto@stratplan.co.jp"
  "井上 瑞希 | Webディレクター | ウェブ・ディレクション・ワークス | 東京都新宿区歌舞伎町 1-1-1 | m.inoue@webdirworks.jp"
  "木村 健太 | セキュリティエンジニア | セキュア・ネットワークス | 東京都品川区大崎 1-1-1 | k.kimura@secnet.jp"
  "林 詩織 | 編集者 | パブリッシング・コア | 東京都文京区後楽 1-1-1 | s.hayashi@pubcore.co.jp"
  "斉藤 亮 | 弁理士 | リーガル・アシスト | 東京都台東区上野 7-1-1 | r.saito@legalassist.jp"
  "清水 夏海 | イラストレーター | デジタル・アート・クリエイション | 東京都墨田区押上 1-1-2 | n.shimizu@digitalart.jp"
)

STYLES=(
  "minimalist and sleek with plenty of white space"
  "modern and bold with vibrant accent colors"
  "luxurious with subtle gold leaf or foil accents on premium textured paper"
  "eco-friendly aesthetic with recycled paper texture and natural earthy tones"
  "high-tech futuristic style with holographic elements and dark background"
  "traditional Japanese fusion with subtle 'wasan' motifs and elegant patterns"
  "creative and artistic with abstract watercolor splashes or geometry"
  "corporate yet trendy with a vertical layout and flat design elements"
)

SURFACES=(
  "a dark mahogany wooden desk"
  "a clean white marble tabletop"
  "a modern industrial concrete surface"
  "a slate gray office desk with soft indirect light"
  "a minimalist glass desk showing subtle reflections"
  "a textured linen surface with a warm, cozy atmosphere"
  "a metallic brushed aluminum workstation"
  "a light oak wood surface with natural sunlight filtering through leaves"
)

for i in "${!CARDS[@]}"; do
  CARD_DATA="${CARDS[$i]}"
  IFS=" | " read -r NAME TITLE COMPANY ADDR EMAIL <<< "$CARD_DATA"

  style_idx=$((i % ${#STYLES[@]}))
  surf_idx=$((i % ${#SURFACES[@]}))
  STYLE="${STYLES[$style_idx]}"
  SURFACE="${SURFACES[$surf_idx]}"

  last_file=$(ls images/*.png 2>/dev/null | sort | tail -n 1)
  if [ -z "$last_file" ]; then
    next_num="001"
  else
    last_num=$(basename "$last_file" .png)
    next_num=$(printf "%03d" $((10#$last_num + 1)))
  fi
  filename="images/${next_num}.png"

  echo "[$((i+1))/20] Generating diverse card for: $NAME ($COMPANY)"
  echo "   Style: $STYLE | Surface: $SURFACE"

  PROMPT="A professional, high-quality photograph of a business card placed on $SURFACE. The business card must clearly display the following information in Japanese:
- Name: $NAME
- Title: $TITLE
- Company: $COMPANY
- Address: $ADDR
- Email: $EMAIL

Design requirements:
- Use a $STYLE.
- The typography must be professional and balanced.
- Ensure the card orientation (horizontal or vertical) matches the style.
- The shot is a close-up depth-of-field photograph with realistic lighting and soft background bokeh."

  RESPONSE=$(curl -s -X POST \
    "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent" \
    -H "x-goog-api-key: $GEMINI_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"contents\": [{
        \"parts\": [
          {\"text\": \"$(echo "$PROMPT" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | tr -d '\n')\"}
        ]
      }]
    }")

  echo "$RESPONSE" | jq -r '.candidates[0].content.parts[] | select(.inlineData) | .inlineData.data' | base64 -d > "$filename"

  if [ -s "$filename" ]; then
    echo "   Successfully saved to $filename"
  else
    echo "   Error: Failed to generate for $NAME"
    echo "$RESPONSE" > "error_${next_num}.json"
    rm -f "$filename"
  fi

  sleep 1
done

これらの名刺画像を使って、Gemini NanoGemini2.5 Flashの両方で情報抽出を行い、結果を比較してみます。

結果

それぞれの出力をシートにまとめ、間違えた箇所を赤背景にしています。

Gemini Nano の正答率

Gemini Nano の正答率は 4/20 という結果になりました。全体的にミスが多いのですが、特に地名に弱そうですね。

Gemini2.5 Flash の正答率

一方 Gemini2.5 Flash は余裕の全問正解となりました。Geminiが作った名簿とGeminiが作った画像なので「知識の範囲のOCR」というところも有利に働いてる可能性はあります。

おわりに

今回はGemini Nanoを使って名刺画像から情報を抽出するタスクを試してみました。
Gemini Nanoはローカルで動作する便利なモデルですが、名刺読み取りのようなタスクではまだ精度が十分とは言えない結果となりました。
一方で、ローカルのCPUで動作していて、しかもかなり高速であるということを考えると、用途によっては十分に実用的であるとも言えます。

Pocket

CONTACT

お問い合わせ・ご依頼はこちらから