リアルタイムでVLM推論してみる

背景や目的


これまでに、VLMに静止画像を与え、その内容を分析するアプリを試作してきました。
(記事:1.VLMで熊問題に対策
    2.Gemma4でVLM利用アプリを試作(SFTあり)

そこから発展して今回は、撮影済みの画像をアップロードして分析するのではなく、スマホのカメラ映像をそのままリアルタイムに処理させてみます。
画像を扱うAIはとても便利で、可能性を感じています。今回はそのアプリを具体的に作る時の基礎情報を得るために、ミニ版を試作してみます。VLMに興味がある方は、ご覧ください。

試作品の概要


今回は、スマホのブラウザで動くアプリにします。ストリーミングを開始すると、カメラ映像をVLMで解析し、その結果文を画面内に表示するアプリを試作します。

アプリの画面は以下のようにしたいです。

最初にカメラ起動ボタンを表示し、ボタン押下後にスマホのバックカメラ映像を表示します。その後、画面をタップすることで映像を推論し、画面下部に防災リスクの説明文を表示します。

実行環境は以下の通りです。
・実行サーバー(端末):スマホの標準ブラウザ(Android/Google Chrome)
・実行環境(サーバ):AWS Lambda関数
・VLM:クラウドAPI

VLMモデルの選別


VLMモデルには、GPT、Gemini、Claude、Gemmaなど多くの種類があります。
大きく分けると、クラウドAPIとして利用するモデルと、OSSとして別途環境構築して使うモデルがあります。

代表的な違いは以下の通りです。

クラウドAPI

項目特徴
モデル例GPT-5.5, Claude Opus 4.8, Gemini 3.5等
セットアップAPIキーのみ
推論速度高速
コスト従量課金が一般的
カスタマイズ 追加学習等は基本的に不可

OSS

項目特徴
モデル例Gemma4, Qwen3-VL, InternVL3.5等
セットアップモデル推論環境の構築が必要
推論速度構築する環境に依存
コスト基本無料(環境の費用はかかる)
カスタマイズファインチューニング可能

今回は推論結果の評価はしないので、低コストなクラウドAPI:GPT-4o miniを使用します。

手順サマリー


今回の試作は、以下の流れで進めます。

  1. コード作成
    アプリに必要なPGファイルを準備します。
  2. デプロイ
    コード作成完了後、AWS Lambdaにコードをデプロイします。
  3. 動作確認
    スマホでブラウザを開き、Lambda 関数URLにアクセスして、カメラ映像をタップし分析を実行します。

コード作成


まずコードを用意します。
今回はAWS Lambdaで動作させるので、Lambda用の以下2ファイルにしておきます。
・lambda_function.py
・scanner.html

事前準備として、OpenAI GPT APIを使用するため、APIキーを用意しておきます。未作成の場合は、OpenAIアカウントでAPIキーを作成してから進めます。

サーバー側PGとしてlambda_function.pyを作成します。
このPGでは、画面から送信されたカメラ画像を受け取り、OpenAI GPT APIへ送信して、解析結果文を返します。

lambda_function.py
import json
import os
import urllib.request
import urllib.error
import base64

OPENAI_API_KEY  = os.environ.get('OPENAI_API_KEY', '')
DEFAULT_MODEL   = os.environ.get('MODEL', 'gpt-4o-mini')
OPENAI_ENDPOINT = 'https://api.openai.com/v1/chat/completions'

SYSTEM_PROMPT = 'あなたは画像を簡潔に説明するアシスタントです。'

DEFAULT_USER_PROMPT = 'この画像に写っている物体を20語以内で簡潔に説明してください。'

# Load HTML once at cold start
HTML_PATH = os.path.join(os.path.dirname(__file__), 'scanner.html')
try:
    with open(HTML_PATH, 'r', encoding='utf-8') as f:
        HTML = f.read()
except FileNotFoundError:
    HTML = '<h1>scanner.html not found in deployment package.</h1>'


def lambda_handler(event, context):
    method = (
        event.get('requestContext', {})
             .get('http', {})
             .get('method', 'GET')
             .upper()
    )

    # ── CORS preflight ──
    if method == 'OPTIONS':
        return _cors_response()

    # ── GET / — serve the HTML app ──
    if method == 'GET':
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'text/html; charset=utf-8',
                'Access-Control-Allow-Origin': '*',
            },
            'body': HTML,
        }

    # ── POST / — scan image ──
    if method == 'POST':
        try:
            raw_body = event.get('body', '{}')
            if event.get('isBase64Encoded', False):
                raw_body = base64.b64decode(raw_body).decode('utf-8')
            body = json.loads(raw_body)
        except Exception as e:
            return _json(400, {'error': f'Invalid request body: {e}'})

        image_b64   = body.get('image', '')
        model       = body.get('model', DEFAULT_MODEL)
        user_prompt = body.get('prompt', DEFAULT_USER_PROMPT)

        if not image_b64:
            return _json(400, {'error': 'Missing image field'})

        if not OPENAI_API_KEY:
            return _json(500, {'error': 'OPENAI_API_KEY not set in environment variables'})

        try:
            result = call_openai(image_b64, model, user_prompt)
            return _json(200, {'result': result})
        except Exception as e:
            return _json(500, {'error': str(e)})

    # ── method not allowed ──
    return _json(405, {'error': f'Method {method} not allowed'})


def call_openai(image_b64, model, user_prompt):
    """Call OpenAI chat completions API using only stdlib urllib."""
    payload = json.dumps({
        'model': model,
        'max_tokens': 512,
        'messages': [
            {
                'role': 'system',
                'content': SYSTEM_PROMPT
            },
            {
                'role': 'user',
                'content': [
                    {
                        'type': 'text',
                        'text': user_prompt
                    },
                    {
                        'type': 'image_url',
                        'image_url': {
                            'url': f'data:image/jpeg;base64,{image_b64}',
                            'detail': 'low'
                        }
                    }
                ]
            }
        ]
    }, ensure_ascii=False).encode('utf-8')

    req = urllib.request.Request(
        OPENAI_ENDPOINT,
        data=payload,
        headers={
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {OPENAI_API_KEY}',
        },
        method='POST'
    )

    with urllib.request.urlopen(req, timeout=30) as res:
        data = json.loads(res.read().decode('utf-8'))

    return data['choices'][0]['message']['content']


def _json(status_code, data):
    return {
        'statusCode': status_code,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',
        },
        'body': json.dumps(data, ensure_ascii=False),
    }


def _cors_response():
    return {
        'statusCode': 200,
        'headers': {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
            'Access-Control-Allow-Headers': 'content-type',
        },
        'body': '',
    }

コード内11〜13行目に、システムプロンプトとユーザープロンプトを設定しました。
今回は、画像内の物体を短く説明する以下の内容にしました。
システムプロンプト

あなたは画像を簡潔に説明するアシスタントです。

ユーザプロンプト

この画像に写っている物体を20語以内で簡潔に説明してください。

次に、画面表示用のHTMLファイル scanner.htmlを作成します。
このHTMLでは、スマホのカメラ映像を表示し、タップした画像をLambdaへ送信します。
Lambdaから返ってきた結果文は、画面内チャットに表示します。

scanner.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>VLMリアルタイム推論アプリ</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=DotGothic16&family=Share+Tech+Mono&display=swap');

  :root {
    --bg: #f5f7fa;
    --panel: #ffffff;
    --border: #e0e4ea;
    --accent: #1a7f5a;
    --accent2: #d93025;
    --accent3: #1a73e8;
    --warn: #e8820c;
    --text: #1c2b3a;
    --dim: #8a9ab0;
    --scan-line: rgba(0,0,0,0.015);
  }

  * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

  body {
    background: var(--bg);
    color: var(--text);
    font-family: 'Share Tech Mono', monospace;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow-x: hidden;
  }

  body::before {
    content: '';
    position: fixed;
    inset: 0;
    background: repeating-linear-gradient(0deg, var(--scan-line) 0px, var(--scan-line) 1px, transparent 1px, transparent 3px);
    pointer-events: none;
    z-index: 999;
  }

  header {
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: var(--panel); box-shadow: 0 1px 3px rgba(0,0,0,0.08);
  }

  .logo {
    font-family: 'DotGothic16', monospace;
    font-size: 16px;
    color: var(--accent);
    letter-spacing: 2px;
    
  }
  .logo span { color: var(--accent2); }

  .status-dot {
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--dim);
    display: inline-block;
    margin-right: 6px;
    transition: all 0.3s;
  }
  .status-dot.ready  { background: var(--accent);  }
  .status-dot.error  { background: var(--accent2);  }
  .status-dot.thinking { background: var(--warn);  animation: pulse 0.5s infinite; }
  @keyframes pulse { 0%,100%{opacity:1}50%{opacity:0.3} }

  .status-text { font-size: 11px; color: var(--dim); letter-spacing: 1px; }

  /* ── camera ── */
  .camera-wrap {
    position: relative;
    width: 100%;
    background: #000;
    aspect-ratio: 4/3;
    overflow: hidden;
    flex-shrink: 0;
    cursor: pointer;
    user-select: none;
  }

  /* tap hint ring */
  .camera-wrap::after {
    content: 'TAP TO SCAN';
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 11px;
    color: var(--accent);
    letter-spacing: 2px;
    opacity: 0;
    transition: opacity 0.4s;
    white-space: nowrap;
    pointer-events: none;
  }
  .camera-wrap.ready::after { opacity: 0.7; }
  .camera-wrap.analyzing::after { content: '解析中...'; opacity: 0.9; color: var(--warn); }

  video { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
  canvas { display: none; }

  /* corner brackets */
  .corner { position: absolute; width: 20px; height: 20px; transition: border-color 0.3s; }
  .corner.tl { top:8px; left:8px;   border-top:2px solid var(--accent); border-left:2px solid var(--accent); }
  .corner.tr { top:8px; right:8px;  border-top:2px solid var(--accent); border-right:2px solid var(--accent); }
  .corner.bl { bottom:8px; left:8px;  border-bottom:2px solid var(--accent); border-left:2px solid var(--accent); }
  .corner.br { bottom:8px; right:8px; border-bottom:2px solid var(--accent); border-right:2px solid var(--accent); }
  .camera-wrap.analyzing .corner { border-color: var(--warn) !important; }

  /* flash overlay on tap */
  .flash {
    position: absolute;
    inset: 0;
    background: white;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.05s;
  }
  .flash.go { opacity: 0.35; }

  /* ripple on tap */
  .ripple {
    position: absolute;
    border-radius: 50%;
    border: 2px solid var(--accent);
    width: 60px; height: 60px;
    pointer-events: none;
    transform: translate(-50%, -50%) scale(0);
    opacity: 1;
  }
  .ripple.animate {
    animation: rippleOut 0.5s ease-out forwards;
  }
  @keyframes rippleOut {
    to { transform: translate(-50%, -50%) scale(3); opacity: 0; }
  }

  /* scan beam — plays once on tap */
  .scan-beam {
    position: absolute;
    left: 0; right: 0;
    height: 2px;
    background: linear-gradient(90deg, transparent, var(--accent), transparent);
    box-shadow: 0 0 12px var(--accent);
    top: 0;
    opacity: 0;
  }
  .scan-beam.go {
    opacity: 1;
    animation: scanDown 0.8s linear forwards;
  }
  @keyframes scanDown { from { top:0% } to { top:100%; opacity:0; } }

  /* no-cam overlay */
  .no-cam {
    position: absolute; inset: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 8px; background: #e8edf2;
  }
  .no-cam span { font-size: 11px; color: var(--dim); letter-spacing: 1px; }

  /* ── controls ── */
  .controls {
    padding: 10px 16px;
    display: flex;
    gap: 8px;
    background: var(--panel); box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    border-bottom: 1px solid var(--border);
    align-items: center;
  }

  .btn {
    font-family: 'Share Tech Mono', monospace;
    font-size: 12px;
    letter-spacing: 1px;
    padding: 10px 16px;
    border: 1px solid var(--dim);
    background: transparent;
    color: var(--dim);
    cursor: pointer;
    transition: all 0.2s;
    flex: 1;
    text-transform: uppercase;
  }
  .btn:active { transform: scale(0.97); }
  .btn.primary { border-color: var(--accent); color: var(--accent);  box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
  .btn.secondary { border-color: var(--border); font-size: 11px; flex: 0 0 auto; padding: 10px 12px; }
  .btn.secondary:active { border-color: var(--accent3); color: var(--accent3); }
  .btn:disabled { opacity: 0.3; cursor: not-allowed; }

  /* ── output panel ── */
  .output-panel {
    flex: 1;
    overflow-y: auto;
    padding: 12px 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .result-card {
    border: 1px solid var(--border);
    background: var(--panel); box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    padding: 12px;
    animation: slideIn 0.3s ease;
    position: relative;
    overflow: hidden;
  }
  .result-card::before {
    content: '';
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 3px;
  }
  .result-card::before       { background: var(--accent3); }
  .result-card.thinking-card::before { background: var(--warn); }
  @keyframes slideIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }

  .card-meta { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
  .card-time  { font-size:10px; color:var(--dim); letter-spacing:1px; }
  .card-badge { font-size:9px; padding:2px 6px; letter-spacing:1px; text-transform:uppercase; }
  .card-badge.result  { color:var(--accent3); border:1px solid var(--accent3); }
  .card-badge.thinking{ color:var(--warn);    border:1px solid var(--warn); }
  .card-body { font-size:13px; line-height:1.7; color:var(--text); white-space:pre-wrap; }

  .thinking-dots::after { content:'.'; animation: dots 1.2s infinite; }
  @keyframes dots { 0%{content:'.'} 33%{content:'..'} 66%{content:'...'} }



  /* ── output label ── */
  .output-label {
    padding: 8px 16px;
    font-size: 10px;
    letter-spacing: 2px;
    color: var(--dim);
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    text-transform: uppercase;
  }

  /* ── empty state ── */
  .empty-state { text-align:center; padding:32px 20px; color:var(--dim); font-size:12px; letter-spacing:1px; line-height:2.2; }
  .empty-state .big { font-family:'DotGothic16',monospace; font-size:36px; color:var(--border); display:block; margin-bottom:8px; }
  .empty-state .hint { font-size:18px; display:block; margin-bottom:8px; animation: bounceY 1.5s infinite; }
  @keyframes bounceY { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
</style>
</head>
<body>

<header>
  <div class="logo">VLM<span>リアルタイム推論</span></div>
  <div style="display:flex;align-items:center;gap:6px">
    <span class="status-dot" id="statusDot"></span>
    <span class="status-text" id="statusText">STANDBY</span>
  </div>
</header>

<div class="camera-wrap" id="cameraWrap" onclick="handleTap(event)">
  <video id="video" autoplay playsinline muted></video>
  <canvas id="canvas"></canvas>
  <div class="corner tl"></div>
  <div class="corner tr"></div>
  <div class="corner bl"></div>
  <div class="corner br"></div>
  <div class="scan-beam" id="scanBeam"></div>
  <div class="flash" id="flash"></div>
  <div class="ripple" id="ripple"></div>
  <div class="no-cam" id="noCam">
    <span>[ カメラ未起動 ]</span>
  </div>
</div>

<div class="controls">
  <button class="btn primary" id="startBtn" onclick="toggleCamera()">▶ カメラ起動</button>
</div>



<div class="output-label">結果表示欄(タップ後処理)</div>
<div class="output-panel" id="outputPanel">
  <div class="empty-state" id="emptyState">
    <span class="hint">👆</span>
    カメラを起動して<br>映像をタップしてスキャン
  </div>
</div>

<script>
const DEFAULT_PROMPT = 'この画像に写っている物体を20語以内で簡潔に説明してください。';

let stream = null;
let analyzing = false;
let facingMode = "environment"; // back camera only
let scanCount = 0;
let cameraReady = false;

const video      = document.getElementById('video');
const canvas     = document.getElementById('canvas');
const ctx        = canvas.getContext('2d');
const startBtn   = document.getElementById('startBtn');
const statusDot  = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const scanBeam   = document.getElementById('scanBeam');
const flash      = document.getElementById('flash');
const ripple     = document.getElementById('ripple');
const cameraWrap = document.getElementById('cameraWrap');
const noCam      = document.getElementById('noCam');
const outputPanel= document.getElementById('outputPanel');
const emptyState = document.getElementById('emptyState');

// ── camera toggle ──
function toggleCamera() {
  if (cameraReady) {
    stopCamera();
  } else {
    initCamera();
  }
}

function stopCamera() {
  if (stream) stream.getTracks().forEach(t => t.stop());
  stream = null;
  cameraReady = false;
  video.srcObject = null;
  noCam.style.display = 'flex';
  cameraWrap.classList.remove('ready', 'analyzing');
  startBtn.textContent = '▶ カメラ起動';
  setStatus('standby', 'STANDBY');
  if (emptyState) emptyState.style.display = 'block';
}

// ── camera ──
async function initCamera() {
  const ok = await startCamera();
  if (ok) {
    startBtn.textContent = '■ カメラ停止';
  }
}

async function startCamera() {
  try {
    if (stream) stream.getTracks().forEach(t => t.stop());
    stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode, width:{ideal:1280}, height:{ideal:720} },
      audio: false
    });
    video.srcObject = stream;
    noCam.style.display = 'none';
    cameraWrap.classList.add('ready');
    cameraReady = true;
    setStatus('ready', 'READY');
    if (emptyState) emptyState.style.display = 'none';
    return true;
  } catch(e) {
    setStatus('error', 'CAM ERROR');
    addResult('error', `カメラにアクセスできません: ${e.message}`);
    return false;
  }
}

// back camera only — flip disabled

// ── tap handler ──
async function handleTap(e) {
  if (!cameraReady) return;
  if (analyzing) return; // block double-tap during analysis

  // show ripple at tap position
  const rect = cameraWrap.getBoundingClientRect();
  const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
  const y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
  ripple.style.left = x + 'px';
  ripple.style.top  = y + 'px';
  ripple.classList.remove('animate');
  void ripple.offsetWidth; // reflow
  ripple.classList.add('animate');

  // flash
  flash.classList.add('go');
  setTimeout(() => flash.classList.remove('go'), 120);

  // scan beam
  scanBeam.classList.remove('go');
  void scanBeam.offsetWidth;
  scanBeam.classList.add('go');

  await doScan();
}

// ── capture & analyze ──
async function doScan() {
  analyzing = true;
  scanCount++;

  cameraWrap.classList.remove('ready');
  cameraWrap.classList.add('analyzing');
  setStatus('thinking', 'ANALYZING...');

  // capture frame
  canvas.width  = video.videoWidth  || 640;
  canvas.height = video.videoHeight || 480;
  ctx.drawImage(video, 0, 0);
  const imageData = canvas.toDataURL('image/jpeg', 0.85).split(',')[1];

  const thinkingCard = addResult('thinking', null);

  const model  = 'gpt-4o-mini';
  const prompt = DEFAULT_PROMPT;

  try {
    // Send image to Colab proxy — API key never leaves the server
    const res = await fetch('/api/scan', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ image: imageData, model, prompt })
    });

    const data = await res.json();

    if (!res.ok) {
      thinkingCard.remove();
      addResult('error', `API エラー: ${data?.error || 'HTTP ' + res.status}`);
      setStatus('error', 'API ERROR');
    } else {
      const text = data.result || '(応答なし)';
      thinkingCard.remove();
      addResult('result', text);
      setStatus('ready', 'READY');
    }

  } catch(e) {
    thinkingCard.remove();
    addResult('error', `ネットワークエラー: ${e.message}`);
    setStatus('error', 'NET ERROR');
  }

  analyzing = false;
  cameraWrap.classList.remove('analyzing');
  cameraWrap.classList.add('ready');
}

// ── UI helpers ──
function setStatus(type, text) {
  statusDot.className = 'status-dot';
  if (type !== 'standby') statusDot.classList.add(type === 'ready' ? 'ready' : type);
  statusText.textContent = text;
}

function addResult(type, text) {
  const card = document.createElement('div');
  card.className = `result-card ${type === 'thinking' ? 'thinking-card' : ''}`;
  const now  = new Date();
  const time = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;

  if (type === 'thinking') {
    card.innerHTML = `
      <div class="card-meta">
        <span class="card-time">${time} — #${scanCount}</span>
        <span class="card-badge thinking">ANALYZING</span>
      </div>
      <div class="card-body thinking-dots">解析中</div>`;
  } else {
    card.innerHTML = `
      <div class="card-meta">
        <span class="card-time">${time} — #${scanCount}</span>
        ${type === 'error' ? '<span class="card-badge thinking">ERROR</span>' : ''}
      </div>
      <div class="card-body">${escHtml(text)}</div>`;
  }
  outputPanel.insertBefore(card, outputPanel.firstChild);
  return card;
}

function escHtml(s) {
  return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}

// ── init ── (no settings to load)
</script>
</body>
</html>

これでPG作成の部分は完了です。

デプロイ


今回は、AWS Lambda 関数上で実行する構成にしました。
次に、AWS Lambda 関数を作成し、先ほど作成したコードを登録します。

AWS上にLambda関数を以下の手順で作成します。
・AWSコンソールでLambdaメニューに遷移し、トップ画面で「関数の作成」を押します。
   (※AWSアカウントの作成など基本設定はここでは割愛します)

・ 画面「関数の作成」では、以下の設定値で、作成します。

設定項目
関数名vlm_realtime ※お好きな名称で
ランタイムpython3.12
アーキテクチャx86_64(デフォルト)
実行ロール新規ロール(デフォルト)
関数 URL を有効化チェック
認証タイプNONE

これでLambda関数作成の部分は完了です。
次は準備したPGをアップして、設定して、デプロイします。

・先ほど作成した2つファイル(lambda_function.pyと1つのHTML)を手元に準備できたら、Lambda関数にセットします。

AWSコンソールのLambda内コードソースは以下のようになっていればOKです。

あわせて、OpenAI GPT APIを使用するためのAPIキーを環境変数に設定します。
・「設定」→「環境変数」→「編集」からキー情報を入力し、「保存」で設定してください。

最後に「Deploy」を押して、コードをデプロイします。

以下の確認メッセージが表示されればOKです。これでセットアップは完了です。

動作確認


スマホ上のブラウザでLambda関数URLにアクセスして動作を確認を早速ためしてみます!

初回アクセスすると、カメラ利用の許可を求めるポップアップが表示されました。
・「サイトへのアクセス時は許可」をクリックします。

その後、カメラを起動し、映像をタップして分析を実行します。
以下は、その動作を確認したデモ動画です。

良いですね!

まとめ


今回は、VLMを使ったリアルタイム画像分析アプリを試作しました。

スマホのブラウザからカメラ映像を取得し、クラウドAPIに送信して分析する流れを確認しました。今回は推論結果の評価は行いませんが、GPTのminiモデルの応答速度は速かったです。

今回得た事として、VLM画像分析アプリを実装する流れを確認できました。読者の方にとっても、実装イメージを持つ参考になれば幸いです。次は、実際の製品で試してみます。
やはり推論の精度が気になるので、ニーズにマッチできるか色々と確認してみます。

VLMದು ಹೆಚ್ಚಿನ ಉಪಯೋಗಗಳನ್ನು ಅನ್ವೇಷಿಸೋಣ
(VLMの活用先をさらに検討しましょう)

ご連絡フォーム


フィードバックを是非お願いします。
本記事の方法での問題点や、よりよい方法のアイデアを頂けると大変助かります。

この記事に関して

その他のご連絡

DevAIsをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む