Ollama+llama3.2-vision:11bを使った画像のメタタグ生成とR18判定の実装。ローカルLLMで無料で画像判定ができる時代の到来
今回は最近でたllama3.2-visionを使って画像をローカルLLMに読み込ませ、画像にあったタグとR18画像かの判定を試してみます。
完成イメージ
タイトルにもあるように画像を読み込ませたら、タグとレーティングをJSONで出力してくれます。
python3 vision2-tag-csv.py /mnt/c/Users/hoge/Downloads/IMG_3634.jpg
Response Details:
Model: llama3.2-vision:11b
Created At (JST): 2024-11-17 21:01:32
Total Duration: 88.00s
Load Duration: 2.74s
Prompt Eval Count: 168
Prompt Eval Duration: 81.14s
Eval Count: 25
Eval Duration: 3.53s
Parsed Content:
Data appended to CSV.
{
"tag": [
"人型",
"海水浴",
"女性",
"水着",
"イメージ"
],
"rating": "general"
}
設計思想
ローカルLLMの画像読み込み機能の使い道の一つがタグ生成とR18判定だと思ったから
特にR18判定はクラウドLLMだと難しいと思われるため利点がある
90bでなく軽量な方の11bなのは大量に画像を処理するには11bのほうが有効だと思われるため
11bだとMacBookAir M1 メモリ16GBでも動く!
最終的には特定のフォルダを読み込ませて画像を大量に処理させたい
準備
Ollamaをローカルにインストールしllama3.2-vision:11bをダウンロードしておく
PythonからはAPI接続でモデルを利用するためOllamaを起動しておくこと
下記でモジュールのインストール
pip install ollama
STEP1 これは何の画像ですか?
まず公式のサンプル(https://ollama.com/blog/llama3.2-vision)を少し改造してコマンドライン引数からファイルを読み込ませるようにし、さらに日本語プロンプトにします
import ollama
import sys
inputImageFile = sys.argv[1]
response = ollama.chat(
model='llama3.2-vision:11b',
messages=[{
'role': 'user',
'content': 'これはなんの画像ですか?',
'images': [inputImageFile]
}]
)
print(response)
下記の画像を読み込ませます
python3 vision1-jp.py XXXX.JPG
レスポンスはこんな形です。Pythonのハッシュのダンプになっています
STEP2 画像のタグを生成するようプロンプトで指示
import ollama
import sys
inputImageFile = sys.argv[1]
response = ollama.chat(
model='llama3.2-vision:11b',
messages=[{
'role': 'user',
'content': '''
あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
例1 {"tag": ["虎", "動物", "公園", "女性","アイス"] }
のような形で。
タグ名は日本語にしてください。
その他の文言は返さないでください。
''',
'images': [inputImageFile]
}]
)
print(response)
これぐらい厳密にプロンプトを書かないとタグを正確に返してくれません。
先ほどのペンギンを入力します
python3 vision2-tag2-jp.py xxxx.JPG
contentにタグ以外が入ってるのが気に入らないですが["ピンギン","動物","海水浴","アイス","女性"] }と帰ってきています。ただ4,5番目のタグはプロンプトに惑わされている感じです。
もう一回同じプログラムを打ち込んでみます
今後は綺麗に ["ペンギン","水","海洋生物","動物","鳥"]と生成しています。
こういった曖昧さは生成AIにはつきものです。
STEP3 出力を綺麗にする。Pythonの変数の整形とJSONパース
プロンプトはそのままにして出力を見やすくします
import ollama
import sys
import json
from datetime import datetime, timedelta, timezone
inputImageFile = sys.argv[1]
# 画像を解析してレスポンスを取得
response = ollama.chat(
model='llama3.2-vision:11b',
messages=[{
'role': 'user',
'content': '''
あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
例1 {"tag": ["虎", "動物", "公園", "女性","アイス"] }
のような形で。
タグ名は日本語にしてください。
JSON以外の文字列は返答しないでください。
''',
'images': [inputImageFile]
}]
)
# 日本時間変換とフォーマット
def convert_to_japan_time(utc_time_str):
# 余分な小数点以下をトリム
if "." in utc_time_str:
utc_time_str = utc_time_str.split(".")[0] + "Z"
# UTC形式をdatetimeオブジェクトに変換し、タイムゾーンを明示
utc_time = datetime.strptime(utc_time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
# 日本時間(UTC+9)に変換
japan_time = utc_time.astimezone(timezone(timedelta(hours=9)))
# 日本時間をフォーマット
return japan_time.strftime("%Y-%m-%d %H:%M:%S")
# レスポンスをきれいに表示
print("Response Details:")
try:
# 時間の変換
created_at_japan = convert_to_japan_time(response['created_at'])
# 秒に変換して表示
total_duration_sec = response['total_duration'] / 1_000_000_000
load_duration_sec = response['load_duration'] / 1_000_000_000
prompt_eval_duration_sec = response['prompt_eval_duration'] / 1_000_000_000
eval_duration_sec = response['eval_duration'] / 1_000_000_000
print(f" Model: {response['model']}")
print(f" Created At (JST): {created_at_japan}")
print(f" Total Duration: {total_duration_sec:.2f}s")
print(f" Load Duration: {load_duration_sec:.2f}s")
print(f" Prompt Eval Count: {response['prompt_eval_count']}")
print(f" Prompt Eval Duration: {prompt_eval_duration_sec:.2f}s")
print(f" Eval Count: {response['eval_count']}")
print(f" Eval Duration: {eval_duration_sec:.2f}s")
# JSONパースを試行
content = response['message']['content']
print("\nParsed Content:")
parsed_json = json.loads(content) # JSONパース
print(json.dumps(parsed_json, indent=2, ensure_ascii=False)) # 見やすく整形して表示
except json.JSONDecodeError:
print("\nError: Failed to parse 'content' as JSON.")
print(f"Raw Content: {response['message']['content']}")
except KeyError as e:
print(f"\nError: Missing key in response: {e}")
except Exception as e:
print(f"\nUnexpected Error: {e}")
今度はこちらの画像を読み込ませてみます
python3 vision2-tag3.py xxxx.png
Response Details:
Model: llama3.2-vision:11b
Created At (JST): 2024-11-17 21:37:01
Total Duration: 86.87s
Load Duration: 3.02s
Prompt Eval Count: 127
Prompt Eval Duration: 80.96s
Eval Count: 18
Eval Duration: 2.65s
Parsed Content:
{
"tag": [
"少女",
"剣",
"赤眼",
"白服",
"黒髪"
]
}
きれいに返ってきてますね!
90bでなくllama3.2-vision:11bでもタグ生成であれば日本語でも十分使えるのがわかると想います。
STEP4 R18判定
プロンプトを改良しR18判定をしてもらいます
import ollama
import sys
import json
from datetime import datetime, timedelta, timezone
inputImageFile = sys.argv[1]
# 画像を解析してレスポンスを取得
response = ollama.chat(
model='llama3.2-vision:11b',
messages=[{
'role': 'user',
'content': '''
あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
またこの画像が成人向けなら "rating": "R18", そうでないなら "rating": "general" の値を加えてください
例1 {"tag": ["虎", "動物", "公園", "女性","アイス"], "rating": "general" }
のような形で。
タグ名は日本語にしてください。
JSON以外の文字列は返答しないでください。
''',
'images': [inputImageFile]
}]
)
# 日本時間変換とフォーマット
def convert_to_japan_time(utc_time_str):
# 余分な小数点以下をトリム
if "." in utc_time_str:
utc_time_str = utc_time_str.split(".")[0] + "Z"
# UTC形式をdatetimeオブジェクトに変換し、タイムゾーンを明示
utc_time = datetime.strptime(utc_time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
# 日本時間(UTC+9)に変換
japan_time = utc_time.astimezone(timezone(timedelta(hours=9)))
# 日本時間をフォーマット
return japan_time.strftime("%Y-%m-%d %H:%M:%S")
# レスポンスをきれいに表示
print("Response Details:")
try:
# 時間の変換
created_at_japan = convert_to_japan_time(response['created_at'])
# 秒に変換して表示
total_duration_sec = response['total_duration'] / 1_000_000_000
load_duration_sec = response['load_duration'] / 1_000_000_000
prompt_eval_duration_sec = response['prompt_eval_duration'] / 1_000_000_000
eval_duration_sec = response['eval_duration'] / 1_000_000_000
print(f" Model: {response['model']}")
print(f" Created At (JST): {created_at_japan}")
print(f" Total Duration: {total_duration_sec:.2f}s")
print(f" Load Duration: {load_duration_sec:.2f}s")
print(f" Prompt Eval Count: {response['prompt_eval_count']}")
print(f" Prompt Eval Duration: {prompt_eval_duration_sec:.2f}s")
print(f" Eval Count: {response['eval_count']}")
print(f" Eval Duration: {eval_duration_sec:.2f}s")
# JSONパースを試行
content = response['message']['content']
print("\nParsed Content:")
parsed_json = json.loads(content) # JSONパース
print(json.dumps(parsed_json, indent=2, ensure_ascii=False)) # 見やすく整形して表示
except json.JSONDecodeError:
print("\nError: Failed to parse 'content' as JSON.")
print(f"Raw Content: {response['message']['content']}")
except KeyError as e:
print(f"\nError: Missing key in response: {e}")
except Exception as e:
print(f"\nUnexpected Error: {e}")
さすがにR18画像はここには乗せられませんが
getchuコムから適当にエロアニメ画像を拾って読み込ませてみました
正しくエロ画像も判定してくれています。それっぽいワードがタグとして並んでいます。これがローカルLLMの強みでしょう
JSONのパースに失敗しています。AI君がおそらくタグ以外にも色々文言を付け加えているためタグのJSONパースに失敗します。
色々というのは「これは成年画像です」などプロンプトが複雑になっているのでそれに呼応して余計なことを返答する回数も増えてしまっています。
STEP5 R18判定をしつつ、LLMの想定外の返答に対応する
llamaが指定したJSONタグ以外の文言をレスポンスしていてもその文言にはたいていJSONタグがはいっているのでそれを読み取る処理も加えます
失敗レスポンス例
import ollama
import sys
import json
import re
from datetime import datetime, timedelta, timezone
inputImageFile = sys.argv[1]
# 画像を解析してレスポンスを取得
response = ollama.chat(
model='llama3.2-vision:11b',
messages=[{
'role': 'user',
'content': '''
あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
またこの画像が成人向けなら "rating": "R18", そうでないなら "rating": "general" の値を加えてください
例1 {"tag": ["虎", "動物", "公園", "女性","アイス"], "rating": "general" }
のような形で。
タグ名は日本語にしてください。
JSON以外の文字列は返答しないでください。
''',
'images': [inputImageFile]
}]
)
# 日本時間変換とフォーマット
def convert_to_japan_time(utc_time_str):
# 余分な小数点以下をトリム
if "." in utc_time_str:
utc_time_str = utc_time_str.split(".")[0] + "Z"
# UTC形式をdatetimeオブジェクトに変換し、タイムゾーンを明示
utc_time = datetime.strptime(utc_time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
# 日本時間(UTC+9)に変換
japan_time = utc_time.astimezone(timezone(timedelta(hours=9)))
# 日本時間をフォーマット
return japan_time.strftime("%Y-%m-%d %H:%M:%S")
# content中からJSON形式を抽出する関数
def extract_json_from_content(content):
json_pattern = r'{.*"tag".*}'
match = re.search(json_pattern, content, re.DOTALL)
if match:
return match.group(0)
return None
# レスポンスをきれいに表示
print("Response Details:")
try:
# 時間の変換
created_at_japan = convert_to_japan_time(response['created_at'])
# 秒に変換して表示
total_duration_sec = response['total_duration'] / 1_000_000_000
load_duration_sec = response['load_duration'] / 1_000_000_000
prompt_eval_duration_sec = response['prompt_eval_duration'] / 1_000_000_000
eval_duration_sec = response['eval_duration'] / 1_000_000_000
print(f" Model: {response['model']}")
print(f" Created At (JST): {created_at_japan}")
print(f" Total Duration: {total_duration_sec:.2f}s")
print(f" Load Duration: {load_duration_sec:.2f}s")
print(f" Prompt Eval Count: {response['prompt_eval_count']}")
print(f" Prompt Eval Duration: {prompt_eval_duration_sec:.2f}s")
print(f" Eval Count: {response['eval_count']}")
print(f" Eval Duration: {eval_duration_sec:.2f}s")
# JSONパースを試行
content = response['message']['content']
print("\nParsed Content:")
try:
parsed_json = json.loads(content) # JSONパース
except json.JSONDecodeError:
# JSONパースエラーが発生した場合、contentからJSON部分を抽出
print(content)
print("\nError: Failed to parse 'content' as JSON. Attempting to extract JSON from content...")
extracted_json = extract_json_from_content(content)
if extracted_json:
print("Extracted JSON found:")
parsed_json = json.loads(extracted_json)
else:
raise ValueError("No valid JSON could be extracted from the content.")
# 抽出・パースしたJSONをきれいに整形して表示
print(json.dumps(parsed_json, indent=2, ensure_ascii=False))
except json.JSONDecodeError:
print("\nError: Unable to parse content into JSON and no valid JSON could be extracted.")
print(f"Raw Content: {response['message']['content']}")
except KeyError as e:
print(f"\nError: Missing key in response: {e}")
except Exception as e:
print(f"\nUnexpected Error: {e}")
これでJSONパースに1度失敗してもタグを拾える様になりました!
R18じゃない画像を読み込ませてみます
Response Details:
Model: llama3.2-vision:11b
Created At (JST): 2024-11-17 19:21:22
Total Duration: 90.72s
Load Duration: 0.08s
Prompt Eval Count: 168
Prompt Eval Duration: 84.27s
Eval Count: 40
Eval Duration: 5.65s
Parsed Content:
Data appended to CSV.
{
"tag": [
"古代",
"博物館",
"像",
"像",
"祀"
],
"rating": "general"
}
ただし信頼は禁物、あくまでも簡易判定ということで正確ではない
こちらのラインが微妙な学園アイドルマスターの画像ですが
R18されたりされなかったりします
1回目 全年齢判定
Response Details:
Model: llama3.2-vision:11b
Created At (JST): 2024-11-17 21:55:20
Total Duration: 96.51s
Load Duration: 2.97s
Prompt Eval Count: 168
Prompt Eval Duration: 83.55s
Eval Count: 62
Eval Duration: 9.70s
Parsed Content:
この画像には以下のタグが含まれます。
{"tag": ["女性", "写真","水着","浴衣","花嫁"], "rating": "general"}
これは成人向け画像ではないので、 rating の値に "general" という文字列を付けました。
Error: Failed to parse 'content' as JSON. Attempting to extract JSON from content...
Extracted JSON found:
{
"tag": [
"女性",
"写真",
"水着",
"浴衣",
"花嫁"
],
"rating": "general"
}
2回目 R18判定
Response Details:
Model: llama3.2-vision:11b
Created At (JST): 2024-11-17 21:56:32
Total Duration: 42.17s
Load Duration: 0.02s
Prompt Eval Count: 168
Prompt Eval Duration: 0.15s
Eval Count: 271
Eval Duration: 41.76s
Parsed Content:
この画像に含まれるタグ名について、5つ選んでJSON形式で書くという質問です。
まず、この画像が成人向けであることから、"rating": "R18" を加えてください。
次に、以下のタグを選択します。
- 「アイドル」: この画像には青い衣装を着た女性が写っていることから、アイドルと呼ばれるようで、タグとして採用します。
- 「温泉」: この画像は、温泉施設の中で撮影されています。タグとして「温泉」を採用します。
- 「水着」: この画像には水着を着た女性が写っており、タグとして「水着」を採用します。
- 「花嫁衣装」: この画像には、白い衣装を着た女性が写っていることから、「花嫁衣装」というタグを選びます。
- 「アイス」: これはこの画像に含まれるアイテムです。
以上の理由により、この画像に関する以下のJSON形式のデータを作成できます。
{"tag": ["アイドル", "温泉","水着","花嫁衣装","アイス"], "rating": "R18" }
Error: Failed to parse 'content' as JSON. Attempting to extract JSON from content...
Extracted JSON found:
{
"tag": [
"アイドル",
"温泉",
"水着",
"花嫁衣装",
"アイス"
],
"rating": "R18"
}
STEP6(完成) バッチで回すことを想定しCSVに結果を書き出す
完成した最終的なコードがこちら
import ollama
import sys
import json
import re
import csv
from datetime import datetime, timedelta, timezone
import os # ファイルの存在確認に使用
inputImageFile = sys.argv[1]
csv_file = "output.csv"
# 画像を解析してレスポンスを取得
response = ollama.chat(
model='llama3.2-vision:11b',
messages=[{
'role': 'user',
'content': '''
あなたは画像を読み込んで内容のタグを出力する役割をもったプログラムです。
なんの画像かを読み込んで、この画像を表すタグ名を5つ選んでJSON形式で書いてください。
またこの画像が成人向けなら "rating": "R18", そうでないなら "rating": "general" の値を加えてください
例1 {"tag": ["虎", "動物", "公園", "女性","アイス"], "rating": "general" }
のような形で。
タグ名は日本語にしてください。
JSON以外の文字列は返答しないでください。
''',
'images': [inputImageFile]
}]
)
# 日本時間変換とフォーマット
def convert_to_japan_time(utc_time_str):
if "." in utc_time_str:
utc_time_str = utc_time_str.split(".")[0] + "Z"
utc_time = datetime.strptime(utc_time_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
japan_time = utc_time.astimezone(timezone(timedelta(hours=9)))
return japan_time.strftime("%Y-%m-%d %H:%M:%S")
# content中からJSON形式を抽出する関数
def extract_json_from_content(content):
json_pattern = r'{.*"tag".*}'
match = re.search(json_pattern, content, re.DOTALL)
if match:
return match.group(0)
return None
# CSV追記書き込み
def append_to_csv(filename, model, created_at, total_duration, load_duration, prompt_eval_count,
prompt_eval_duration, eval_count, eval_duration, content, tags, rating):
# ヘッダー行を定義
headers = [
"filename", "Model", "Created At (JST)", "Total Duration", "Load Duration",
"Prompt Eval Count", "Prompt Eval Duration", "Eval Count", "Eval Duration",
"content", "tag1", "tag2", "tag3", "tag4", "tag5", "rating"
]
# ファイルが存在しない場合はヘッダーを追加
file_exists = os.path.isfile(csv_file)
with open(csv_file, mode="a", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
if not file_exists:
writer.writerow(headers) # ヘッダー行の書き込み
# データ行の書き込み
row = [
filename, model, created_at, f"{total_duration:.2f}s", f"{load_duration:.2f}s",
prompt_eval_count, f"{prompt_eval_duration:.2f}s", eval_count, f"{eval_duration:.2f}s",
content
] + tags + [""] * (5 - len(tags)) + [rating] # タグが5つ未満の場合は空文字を追加
writer.writerow(row)
# レスポンスをきれいに表示
print("Response Details:")
try:
created_at_japan = convert_to_japan_time(response['created_at'])
total_duration_sec = response['total_duration'] / 1_000_000_000
load_duration_sec = response['load_duration'] / 1_000_000_000
prompt_eval_duration_sec = response['prompt_eval_duration'] / 1_000_000_000
eval_duration_sec = response['eval_duration'] / 1_000_000_000
print(f" Model: {response['model']}")
print(f" Created At (JST): {created_at_japan}")
print(f" Total Duration: {total_duration_sec:.2f}s")
print(f" Load Duration: {load_duration_sec:.2f}s")
print(f" Prompt Eval Count: {response['prompt_eval_count']}")
print(f" Prompt Eval Duration: {prompt_eval_duration_sec:.2f}s")
print(f" Eval Count: {response['eval_count']}")
print(f" Eval Duration: {eval_duration_sec:.2f}s")
content = response['message']['content']
print("\nParsed Content:")
tags = []
rating = ""
try:
parsed_json = json.loads(content)
tags = parsed_json.get("tag", [])
rating = parsed_json.get("rating", "")
except json.JSONDecodeError:
print(content)
print("\nError: Failed to parse 'content' as JSON. Attempting to extract JSON from content...")
extracted_json = extract_json_from_content(content)
if extracted_json:
print("Extracted JSON found:")
parsed_json = json.loads(extracted_json)
tags = parsed_json.get("tag", [])
rating = parsed_json.get("rating", "")
else:
print("No valid JSON could be extracted from the content.")
finally:
# CSV書き込み
append_to_csv(
filename=inputImageFile,
model=response['model'],
created_at=created_at_japan,
total_duration=total_duration_sec,
load_duration=load_duration_sec,
prompt_eval_count=response['prompt_eval_count'],
prompt_eval_duration=prompt_eval_duration_sec,
eval_count=response['eval_count'],
eval_duration=eval_duration_sec,
content=content,
tags=tags,
rating=rating
)
print("Data appended to CSV.")
print(json.dumps({"tag": tags, "rating": rating}, indent=2, ensure_ascii=False))
except Exception as e:
print(f"\nUnexpected Error: {e}")
こんなCSVができあがります
まとめ
(電気代を無視すれば)ローカルLLMで画像のタグ生成、R18判定ができるいい時代になりました。クラウドAPIの料金を気にせずに自由にプログラミングできるのはかなりいいのではないでしょうか?
まだまだスピードには難ありですが、他にもいろんな使い道がありそうなので今後も模索していこうと想います。
この記事が気に入ったらサポートをしてみませんか?