Google Apps ScriptでGeminiを使って植物の識別をしてみた【GAS】

AppSheetで植物図鑑に機能を追加したく、「植物の写真から植物名を識別して同定することはできるだろうか?」と考え、Gemini 1.5 Flashのマルチモーダルを利用して、写真をアップしつつ回答を得るというものを作成することができました。

今回はGASの部分だけですが、これをAppSheet側から叩いて答えをAppSheetに返すということがこれで出来るのではないかと思っています。

AppSheetでGemini連携植物図鑑を作ってみた【GAS】

今回利用するサービス等

スマートフォンアプリでは、GreenSnapなどのSNS兼植物識別機能付きアプリというのはたくさん存在しています。しかしこれらはそのサービス内で閉じてる環境であるため、自分が独自に実装したいと思っても、膨大な写真データと識別する仕組みなど到底作れない。

しかし、Geminiを使えばGoogleの検索エンジンデータをそのまま利用して、一定の判定結果を得ることが出来るのではないか?と考えて構築しました。手持ちのいくつかの写真は見事に判定することができました。

主に判定に使った写真たちは以下のもの

図:判定に利用した写真たち

事前準備

GeminiのAPIキーを取得する

GeminiのAPIキーが必要です。以下のエントリーに独立してまとめています。課金されますので利用のしすぎには要注意。また課金されていない場合、学習に利用されてしまう恐れがあるので、きちんとGoogle Cloud上で課金アカウントとの紐付けなどをしておきましょう。

GeminiのAPIキーの取得と学習の可否

スクリプトプロパティの準備

今回は特に設定保存用の機能を装備していないので、スクリプトプロパティに直接設定値を書き込みます。

  • apikeyというプロパティを用意して、前述で用意したAPIキーの値を入れる

これでGAS側の事前準備はオッケー。

図:スクリプトプロパティに設定値を書き込む。

ソースコード

概要

Gemini APIはすでにHTTPリクエスト時にプロンプトの文字だけでなく同時に画像もアップできるようになっています。この添付した画像に対しての指示を与えることで、植物名と参考になるURLを提示させるようにしています。

ただ、ChatGPTと比較するとなかなか融通が利かない面が強く、コードの側で補正してあげなければならない所が結構あります。プロンプト一発でビシっと決まらない点に対するコードを書くというバッドノウハウが必要です。

GCP側のAPIとして利用していないので、プロジェクトの紐付け作業は不要です。

生成プロンプト

今回は植物の画像から植物の名前を判定させるわけですが、必ずしも一発で目的の画像であると判定するのは人間もAIも厳しいです。ということで

  • 値はJSON形式で、回答部分は配列で返してもらうようにしています。
  • 複数候補がある場合を想定して、item以下にはケースによっては複数の植物名が入ってくることがあります。
  • 生成内容にありがちな「余計なコメント」についても出力しないように指示を出しています。
  • またこれもありがちですが、「---」や「`」という回答内容の上下に出てくる余計なワードも除外していますが、あまり言うことを聞いてくれません。
//プロンプトを用意
  let prompt = "添付した画像の植物の画像から、植物の名前の候補を教えてください。"
             + "但し、以下の内容を満たした形で答えを返してください。\n" 
             + "・名前のみ回答してください。説明文や解説・補足は一切必要ありません。"
             + "・回答はJSON形式で、keyはitemとし、この中に配列で{plantname:植物名, url:URL}として列挙してください。"
             + "・回答内のURL部分は参考になるWebサイトのURLを入れてください。"
             + "・回答候補が複数ある場合には同様に配列の中にカンマ区切りで含めてください"
             + "・回答には「---」や「```」、「json\n」といった文字は含めないでください"

コード

以下は画像をアップして判定し答えをJSONで取得するコードになります。ポイントとしては

  • 画像のファイルIDは冒頭のimageidの変数の中に入れておく
  • スクリプトプロパティからapikeyを取得してエンドポイントURLを構築する
  • プロンプトに於いてこれまでのテキスト以外に「inlineData」にて画像を追加で指定します。
  • 画像はドライブから取得後にBase64でエンコードして渡す必要性があります。
  • レスポンスデータから余計な文字列を除外してJSON.parseします。
//画像ファイルのID
var imageid = "ここに画像ファイルのIDを入れる"

//Gemini Endpoint
var endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key="

//Geminiで画像を元に植物名判定
function geminiplant() {
  //APIキーを取得
  let prop = PropertiesService.getScriptProperties();
  let apikey = prop.getProperty("apikey");

  //エンドポイント構築
  let url = endpoint + apikey

  //画像ファイルを読み込み
  let image = DriveApp.getFileById(imageid).getBlob().getBytes();

  //プロンプトを用意
  let prompt = "添付した画像の植物の画像から、植物の名前の候補を教えてください。"
             + "但し、以下の内容を満たした形で答えを返してください。\n" 
             + "・名前のみ回答してください。説明文や解説・補足は一切必要ありません。"
             + "・回答はJSON形式で、keyはitemとし、この中に配列で{plantname:植物名, url:URL}として列挙してください。"
             + "・回答内のURL部分は参考になるWebサイトのURLを入れてください。"
             + "・回答候補が複数ある場合には同様に配列の中にカンマ区切りで含めてください"
             + "・回答には「---」や「```」、「json\n」といった文字は含めないでください"

  //コンテンツを構成
  let contents = [
    {
      "parts": [
        {"text": prompt}, 
        {"inlineData": 
          {
            "mimeType": "image/jpeg", 
            "data": Utilities.base64Encode(image)
          }
        }
      ]
    }
  ];

  //リクエストボディを作成
  const payload = {
    'contents': contents,
    'generationConfig': {
      'maxOutputTokens': 2048,
      'temperature': 0.9
    }
  };

  //リクエストオプション
  const options = {
    'method' : 'POST',
    'contentType':'application/json',
    'payload': JSON.stringify(payload),
    'muteHttpExceptions': true
  }

  //HTTPリクエスト
  const response = UrlFetchApp.fetch(url, options);

  //レスポンスから取得
  const rescode = response.getResponseCode();

  //レスポンスデータを取り出す
  const content = JSON.parse(response.getContentText());

  //内容を取得
  let restext = content.candidates[0].content.parts[0].text;

  //内容から余計な文字を除外する
  restext = restext.replace(/```/g,"");
  restext = restext.replace(/\\n/g, '');
  restext = restext.replace(/json/g, '');

  //JSONとしてパースする
  let json = JSON.parse(restext)

  //植物名を表示する
  let plant = json.item;    //配列を取得する

  for(let i = 0;i<plant.length;i++){
    let result = plant[i].plantname;
    console.log(result)
  }

}

実行結果

レスポンスデータ

Geminiからのレスポンスおよびそれから余計なものを除外した結果が以下のように表示されます。複数候補がある場合には、itemの配列の中に複数のデータが入ってくることになります。

{ item: 
    [
        {
            plantname: 'コーヒーノキ',
            url: 'https: //ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%92%E3%83%BC%E3%83%8E%E3%82%AD' 
        } 
    ] 
}

判定結果

実際に上記のコードで、緑色の物体である「瑠璃兜」というのを育てていますが、非常に種類が豊富でこれは流石に難しいかなと思いましたが、アストロフィツムという学名と近縁種の兜丸であるというのは判定できました。

また他の画像に関してはピタリと判定が出来ているので、なかなかの鑑定眼ではないかと思います。

他の画像は、ブーゲンビリアリュウガンブラシノキとなかなかのものも判定が出来ていたりします。

図:候補として2つ目でバッチリのものが出てきた。

注意点

画像判定系はまだ発展途上の段階です。今回植物に関してはいい線行っていましたが、例えば「キノコの同定」は失敗しています。画像判定して「食毒判定」は非常に危険です。例えば、欧州では缶詰にされて売られてるキノコに「シャグマアミガサタケ」というものがあります。

非常に特徴的なキノコで他に類がない見た目なのですが、これ「猛毒」のキノコです。欧州では毒抜きして食べられてるのですが、そのまま調理して食べると死にます(調理中に発生する毒ガスも非常に危険です)。人間にとっては容易に見分けのつくものでも、キノコを画像判定は非常に問題があります。ましてやツキヨタケのような人間でも見分けが難しいものになったら尚更です。

よって、こういった生成AIを使う場所というものはよく考えて利用する必要があります。これはキノコに限らず、同じ植物でもドクウツギなどは実が若い時と熟した時では大分異なるため、誤判定します。

類似で見間違うセリとドクゼリニラとスイセンニリンソウとトリカブトのように、見た目はそっくりだけれども、匂いや葉の巻き方、根っこの形状など見えない場所まで含めて本来は同定作業というのものを行うので、写真判定して「これは食べられる」というのは非常に危険な発想です。そしてそれはこれから先も生成AIでは難しいのではないかと。

図:シャグマアミガサタケという猛毒キノコ

図:誤判定してる

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)