Ollama × Gemma × Pokemon API で画像認識を試す

スポンサーリンク
Uncategorized

はじめに

どうやらGemmaで画像認識ができるらしい。

そして、GemmaはOllamaで使えるらしい。

よって、無料で画像認識ができるらしい。

そこでどれくらいの画像認識ができるのかを試してみたというのが今回の記事になる。

画像認識について

pokemonのAPIでは公式のポケモンの画像がofficial-artworkとして提供されている。

例えば、フシギバナなら以下の画像リンクとなり、

https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/3.png

以下の画像となる。

この画像の画像認識をOllamaのGemmaにしてもらう。

画像認識はそのポケモンが浮いているか地についているかを判断してもらう。

判断してもらうプログラムはJavaScriptで書く。

プログラムは以下のようにした。

import { readFile, writeFile } from 'node:fs/promises'

const OLLAMA_URL = 'http://localhost:11434/api/chat'
const MODEL = 'gemma3:4b'
const CONCURRENCY = 2
const DATA_PATH = new URL('../src/data/pokemon-gen1-3.json', import.meta.url)

const PROMPT = `この画像は透明背景のポケモンのドット絵スプライトです。
このポケモンは地面から離れて宙に浮いている・浮遊している・飛行している状態ですか?
それとも足や体が地面についている状態ですか?
次のJSON形式だけで答えてください: {"floating": true または false}`

async function fetchImageBase64(url) {
  const res = await fetch(url)
  const buf = Buffer.from(await res.arrayBuffer())
  return buf.toString('base64')
}

async function labelOne(entry) {
  const imageUrl = entry.officialArtworkUrl || entry.imageUrl
  if (!imageUrl) return null

  const image = await fetchImageBase64(imageUrl)
  const res = await fetch(OLLAMA_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: MODEL,
      stream: false,
      format: { type: 'object', properties: { floating: { type: 'boolean' } }, required: ['floating'] },
      messages: [{ role: 'user', content: PROMPT, images: [image] }],
    }),
  })
  const data = await res.json()
  const parsed = JSON.parse(data.message.content)
  return parsed.floating
}

async function main() {
  const entries = JSON.parse(await readFile(DATA_PATH, 'utf-8'))
  const retryOnly = process.argv.includes('--retry-failed')
  const targets = retryOnly ? entries.filter(e => e.floating == null) : entries
  console.log(`processing ${targets.length}/${entries.length} entries${retryOnly ? ' (retry-failed mode)' : ''}`)

  let cursor = 0
  async function worker() {
    while (cursor < targets.length) {
      const idx = cursor++
      const entry = targets[idx]
      try {
        entry.floating = await labelOne(entry)
      } catch (err) {
        entry.floating = null
        console.error(`\n${entry.name} failed: ${err.message}`)
      }
      process.stdout.write(`\r${idx + 1}/${targets.length} (${entry.name}: ${entry.floating})`.padEnd(60))
    }
  }
  await Promise.all(Array.from({ length: CONCURRENCY }, worker))
  console.log()

  await writeFile(DATA_PATH, JSON.stringify(entries, null, 2))
  console.log(`updated ${entries.length} entries in ${DATA_PATH.pathname}`)
}

main()

使用しているgemmaのモデルはgemma3:4b。

labelOne関数でOllamaにアクセスして、画像認識をお願いしている。

実施対象はポケットモンスター赤緑、金銀、ルビーサファイアのポケモンを対象としている。

実行結果について

386匹のポケモンに対してGPUも積んでいないPCで実行したら、7時間ぐらいかかった(笑)

精度について、ざっと確認してみる。

浮いているはずなのに浮いていないと判断されたポケモン、

浮いていないのに浮いていると判断されたポケモンは以下になる。

浮いているはずなのに浮いていないと判断されたポケモン

  • スピアー
  • ピジョン
  • オニドリル
  • ズバット
  • ゴルバット
  • モルフォン
  • ゴース
  • ゴースト
  • ドガース
  • マタドガス
  • フリーザー
  • サンダー
  • ファイヤー
  • レディアン
  • ワタッコ
  • ヤンヤンマ
  • ムウマ
  • グライガー
  • マンタイン
  • ホウオウ
  • セルビィ
  • アゲハント
  • ドクケイル
  • ペリッパー
  • チリーン
  • オニゴーリ
  • ラティアス
  • ラティオス
  • レックウザ

浮いていないのに浮いていると判断されたポケモン

  • ジュゴン
  • バネブー
  • ナマズン

といった具合になった。

浮いているはずなのに浮いていないと判断されたポケモンが多い結果となってしまった。。。

最後に

Gemmaの画像認識の実用性は感じられなかったけど、得意不得意があるのかもしれない。

タイトルとURLをコピーしました