
前回は、OllamaでVL(Vision Language)に対応したモデルを導入し、UIから画像を解析する例を紹介しました。チャット画面を通じて画像を渡し、その内容を自然言語で説明させるところまでを確認しています。
今回は一歩進めて、APIとPythonを組み合わせた実装を試してみます。UIを介さずプログラムから直接画像を送信し、解析結果を取得できれば、定点観測や自動処理への応用が可能になります。
単なるデモではなく、実際に動かせる仕組みとしてどこまで使えるのかを検証していきます。
APIから直接Visionモデルを呼び出すコード例
内蔵カメラを定点観測し、解析結果を出力するコードをChatGPTに生成してもらいました。Macであれば内蔵カメラを利用できますし、iPhoneを連携カメラとして使える環境の方も多いでしょう。複数のカメラがある場合に備え、CLIからカメラを選択できる機能も追加しています。
プロンプトは以下の内容で生成しました。
macOSの環境で、カメラ映像を取得してOllamaのVL対応モデルで解析するPythonコードを生成してほしい。画像の解析は開始と終了の時間が分かるようにし、解析中の画像は画面に表示する。
なおカメラが複数ある場合は選択できるようにしたい。
生成されたコードでは OpenCV を使用しています。事前に必要なモジュールをインストールしておきます。
pip install opencv-python requests
また、Ollamaはあらかじめ起動し、VL対応モデルをダウンロードして実行可能な状態にしておきます。
今回は qwen3-vl を使用しました。4B と 8B の両方を試しましたが、今回のWindows+RTX4070の環境では 4B のほうが不思議と解析に時間がかかり、出力内容もやや安定しているように感じました。この傾向は入力画像や実行環境によって変わる可能性があります。
llava や llama 系のモデルも試しましたが、認識内容そのものは悪くないものの、日本語の出力品質はやや不安定に感じられました。
import cv2
import time
import base64
import requests
import argparse
from datetime import datetime
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "qwen3-vl:4b" # 例: 手元のvisionモデル名に合わせて変更
def ts():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def probe_cameras(max_index=5):
"""Try opening camera indices [0..max_index] and return the ones that open."""
available = []
for idx in range(max_index + 1):
cap = cv2.VideoCapture(idx, cv2.CAP_AVFOUNDATION)
if cap.isOpened():
available.append(idx)
cap.release()
return available
def frame_to_b64jpg(frame, max_w=512):
h, w = frame.shape[:2]
if w > max_w:
new_h = int(h * (max_w / w))
frame = cv2.resize(frame, (max_w, new_h), interpolation=cv2.INTER_AREA)
ok, buf = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 85])
if not ok:
raise RuntimeError("JPEG encode failed")
return base64.b64encode(buf).decode("utf-8")
def ollama_vision(prompt, b64jpg, stream=False, timeout=180):
payload = {
"model": MODEL,
"prompt": prompt,
"images": [b64jpg], # REST APIはbase64画像
"stream": stream,
}
r = requests.post(OLLAMA_URL, json=payload, timeout=timeout)
r.raise_for_status()
data = r.json()
return data.get("response", ""), data
def main():
parser = argparse.ArgumentParser(description="Mac camera -> Ollama Vision demo")
parser.add_argument("--camera", type=int, default=0, help="Camera index (default: 0)")
parser.add_argument("--list-cameras", action="store_true", help="Probe and list available camera indices")
parser.add_argument("--every", type=float, default=1.0, help="Analyze every N seconds (default: 1.0)")
parser.add_argument("--max-w", type=int, default=512, help="Resize analyzed frame max width (default: 512)")
args = parser.parse_args()
if args.list_cameras:
cams = probe_cameras(8)
print(f"[{ts()}] Available camera indices (probe): {cams if cams else 'None found'}")
return
cam_index = args.camera
# MacではAVFoundationを明示すると安定しやすいことが多い
cap = cv2.VideoCapture(cam_index, cv2.CAP_AVFOUNDATION)
if not cap.isOpened():
cams = probe_cameras(8)
raise RuntimeError(
f"Could not open camera index {cam_index}. "
f"Try --list-cameras. Probe result: {cams if cams else 'None'} "
"(Also check macOS camera permission for the app running Python.)"
)
prompt = "この画像で起きていることを日本語で1〜2文で説明して。"
every_sec = float(args.every) # 解析頻度(秒)
next_t = 0.0
print(f"[{ts()}] START camera={cam_index} every={every_sec}s model={MODEL} ollama={OLLAMA_URL}")
while True:
ok, frame = cap.read()
if not ok:
print(f"[{ts()}] Camera read failed")
break
# Live preview
cv2.imshow("camera (live)", frame)
now = time.time()
if now >= next_t:
next_t = now + every_sec
# Prepare the exact frame we will analyze (resize only for analysis)
analyze_frame = frame.copy()
# Show what is being analyzed (flush UI before blocking request)
preview = analyze_frame.copy()
cv2.putText(preview, "ANALYZING...", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2)
cv2.imshow("frame being analyzed", preview)
cv2.waitKey(1)
t0 = time.time()
print(f"[{ts()}] >>> ANALYSIS START")
try:
b64jpg = frame_to_b64jpg(analyze_frame, max_w=args.max_w)
text, _raw = ollama_vision(prompt, b64jpg)
dt = time.time() - t0
print(f"[{ts()}] <<< ANALYSIS END elapsed={dt:.2f}s")
print("VL:", text.strip())
# Update analyzed frame window to indicate completion
done = analyze_frame.copy()
cv2.putText(done, f"DONE ({dt:.2f}s)", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2)
cv2.imshow("frame being analyzed", done)
except Exception as e:
dt = time.time() - t0
print(f"[{ts()}] !!! ANALYSIS ERROR elapsed={dt:.2f}s error={e}")
# q to quit
if cv2.waitKey(1) & 0xFF == ord("q"):
print(f"[{ts()}] QUIT")
break
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
出力例
下記はqwen3-vl:4bの出力例です。Macbookの内蔵カメラを利用したため、編集者と背景が解析されています。編集者の動作に合わせて多少、表現が変わっているのがわかると思います。(日時はマスクしてありますが、出力された解析結果はそのまま利用)下記は qwen3-vl:4b の出力例です。MacBookの内蔵カメラを使用しているため、編集者本人と背景の様子が解析対象となっています。
編集者の動きに応じて表現が微妙に変化していることが分かるでしょう。なお、日時はマスクしていますが、解析結果のテキスト自体はそのまま掲載しています。
今回はWindows+RTX4070の(VRAM 12GB)のリモートサーバーを利用しましたが、画像1枚の解析に数十秒かかるため、リアルタイムの解析は厳しいでしょう。他のモデルも試しましたが、この環境ではこれぐらいが品質と解析時間のバランスが良いようです。
[YYYY-MM-DD HH:MM:SS] START camera=1 every=1.0s model=qwen3-vl:4b ollama=http://XXX.XXX.XXX.XXX:11434/api/generate
[YYYY-MM-DD HH:MM:SS] >>> ANALYSIS START
[YYYY-MM-DD HH:MM:SS] <<< ANALYSIS END elapsed=49.71s
VL: 画像には、白いフード付きスウェットを着た人物が、本やボックス、ぬいぐるみなどが整然と並んだ書架の前に座っている様子が写っています。
[YYYY-MM-DD HH:MM:SS] >>> ANALYSIS START
[YYYY-MM-DD HH:MM:SS] <<< ANALYSIS END elapsed=45.99s
VL: 白いフーディを着た人が、本や文房具、写真立てなどが詰まった本棚の前に座り、手で首を支えながらカメラに向かっている。
[YYYY-MM-DD HH:MM:SS] >>> ANALYSIS START
[YYYY-MM-DD HH:MM:SS] <<< ANALYSIS END elapsed=71.97s
VL: この画像では、白いフード付きパーカーを着た人物が、本や文書、小物が並んだ大型の書架の前に座っている様子が写っています。
まとめ
OpenCVとOllamaのVL対応モデルを組み合わせることで、ローカル環境でもカメラ画像の定点観測が可能になります。UIを介さずAPI経由で画像を解析できるため、自動処理やログ取得などへの応用も現実的です。
一方で、今回の環境では1枚あたり数十秒の解析時間がかかっており、リアルタイム用途にはまだ厳しい面もあります。モデルサイズやGPU性能とのバランスを見ながら、用途に応じた設計が必要でしょう。
それでも、ローカルLLMが「見る」能力を持ち、外界を定期的に観測できるという事実は大きな意味を持ちます。ここから先は、観測した情報をどう活用するかという設計の問題になります。



