Vポイントマーケティング|TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

Codexに「見る力」を与える!Jetsonに接続したUSBカメラをCodex CLIから使ってみました。

こんにちは、VポイントマーケティングAIエンジニアの三浦です。

私はプライベートではJetson AGX Orinの開発者キットにリモートで接続してCodex CLIでコーディングをしたりしています。Jetson AGX Orinは小型でありながらGPUを使ったリアルタイムの映像処理が得意なエッジデバイスで、IoTの領域で力を発揮します。最近は開発サーバーとして活躍してもらっていますが、久しぶりにカメラを使った何か面白いことをやってみたいな、と考えました。

以前JetsonでUSBカメラを接続して、Pythonのプログラムからアクセスして画像を取得する、といったことをやったことがあったのですが、Codexで画像を取得するスクリプトを実行するAgent SKillを作ればCodexが目の前の状態を把握したうえでタスクをこなしてくれるような、Codexに「見る力」を与えることができるんじゃないか・・・と思いました。

実際にやってみるとCodexからカメラにアクセスするスクリプトを単純に呼び出すだけでは実現が難しいことがわかりました。その辺りも含めて今回の記事でまとめてみたいと思います。

CodexをインストールしたJetson AGX OrinにUSBカメラを接続

skill-creatorを使って作ってみる

CodexにはAgent Skillを自然言語で作成できるskill-creatorというスキルがあらかじめ組み込まれています。これを使って以下のように指示を出して作成をしてみました。

$skill-creator いま接続しているUSBカメラから画像を取得することができるSkillを作って。

すると以下のような構成のSkillが生成されました。

CAPTURE-USB-CAMERA
├── agents
│   └── openai.yaml
├── references
│   └── troubleshooting.md
├── scripts
│   ├── __pycache__
│   └── capture_usb_camera.py
└── SKILL.md

agents/openai.yamlって見慣れないな、と思って調べたところ、Codex Appでの表示に関する設定のようでした。このSkillのコアはscripts/capture_usb_camera.pymain()の中身を見るとカメラデバイスを探して使えるバックエンド(gstreamer, ffmpegなど)をコマンドで実行して画像を取得する内容が実装されていることがわかります。

def main() -> int:
    args = parse_args()
    devices = discover_devices()

    if args.list:
        return list_devices(devices, args.json)

    output_path = Path(args.output).expanduser().resolve()
    validate_output_path(output_path, args.overwrite)
    device = select_device(devices, args.device)
    backend = choose_backend(args.backend)

    if backend == "opencv":
        capture_with_opencv(device, output_path, args.width, args.height, args.dry_run)
    else:
        capture_with_command(
            backend,
            device,
            output_path,
            args.width,
            args.height,
            args.timeout,
            args.dry_run,
        )

    if args.dry_run:
        return 0

    print(f"Saved image to {output_path}")
    return 0

実行・・・動かない!?

ということで、ためしにCodexで以下のようにSkillを呼び出してみました。

$capture-usb-camera いま何が写ってる?

しかし、Codexからは以下のようにデバイスが見えていないといったメッセージが返り、Skillを実行することができませんでした。

/dev/video0 /dev/video1 がこの実行環境から見えていません。そのため、Skillはカメラの存在までは検出できていますが、実際の画像取得はまだできません。

もう少し調べてみると、Codexのセッション内では/devが別のファイルシステムに置き換えられていることが要因であることが見えてきました。

- しかしこのセッション内では /dev が別の tmpfs に差し替えられていて、実デバイスノードが見えていません。

Codexのサンドボックスの設定が効いているようです。一番簡単な対応方法はCodexのサンドボックス制御をdanger-full-accessに切り替えることで、実際にその方法でCodexを起動するとSkillを動かすことができました。

ただ、端末全体にかなり強い権限でアクセスできる状態を想定したSkillはあまり望ましいものではないように思います。そこでちょっと方法を見直すことにしました。

リクエスト形式で画像を取得できるサービスを使う

Codexとは別に、リクエストを受け付けると画像を取得するサービスをホスト側で起動しておき、それをSkillで呼び出す形式を考えてみました。Codexは画像が必要な時にリクエストをファイルの形で発行し、画像取得サービスはリクエストファイルを検知すると画像取得を行い、画像ファイルをCodexが見れる場所に格納します。Codexはそのファイルを読めばOK,という流れです。

図にすると以下のような感じです。

CameraWatcher側のプログラムはこのようにしました。

# host_camera_watcher.py
import pathlib
import subprocess
import time

ROOT = pathlib.Path(__file__).resolve().parent
REQ_DIR = ROOT / "camera_requests"
CAPTURE_DIR = ROOT / "captures"
DEVICE = "/dev/video0"

REQ_DIR.mkdir(exist_ok=True)
CAPTURE_DIR.mkdir(exist_ok=True)

print("watching camera_requests/ ...")

while True:
    for req in REQ_DIR.glob("*.request"):
        out = CAPTURE_DIR / (req.stem + ".jpg")
        err = CAPTURE_DIR / (req.stem + ".error.txt")

        try:
            subprocess.run(
                [
                    "ffmpeg",
                    "-hide_banner",
                    "-loglevel", "error",
                    "-f", "v4l2",
                    "-i", DEVICE,
                    "-frames:v", "1",
                    "-y", str(out),
                ],
                check=True,
                timeout=10,
                capture_output=True,
                text=True,
            )
            req.rename(req.with_suffix(".done"))
            print(f"captured {out}")
        except Exception as e:
            err.write_text(str(e))
            req.rename(req.with_suffix(".failed"))
            print(f"failed {req}: {e}")

    time.sleep(0.5)

このサービスにアクセスするための手順(撮影手順)をSKILL.mdに以下のように書いてみました。

## 撮影手順

撮影を要求するときは、次を実行する。

```bash
mkdir -p camera_requests captures
TS="$(date +%s)"
REQ="camera_requests/capture-${TS}.request"
echo "capture" > "$REQ"
echo "$REQ"
```

その後、対応する出力ファイルを確認する。

```bash
OUT="captures/capture-${TS}.jpg"
test -f "$OUT" && echo "$OUT"
```

これでCodexでUSBカメラを使うことができるようになりました。

とりあえずカメラの位置を調整して、カメラの前で自分の服が写るようにして以下のように聞いてみました。

これから東京の町に買い物にいくんだけど、この恰好暑いかな?写真とって確認して。

以下の回答が得られました。

写真では厚手の長袖パーカーに見えます。2026年5月17日の東京は昼間 29〜30°C 前後だったので、買い物で歩くならかなり熱がこもりやすいはずです。電車や店内は冷えることもありますが、外を歩く前提なら 半袖 かかなり薄手の上着 にした方が無難です。少なくともそのパーカーを着たまま長く歩くのは暑い寄りです。
夕方以降まで出るなら、脱ぎやすい羽織りを持つ形がちょうどいいです。

たしかに暑かったです。USBカメラを使うことで開発作業だけでなく、現実の問題でもCodexのサポートが得られるかもしれません。

まとめ

ということで、今回はJetsonにUSBカメラを接続し、Codexからカメラ画像を取得して回答してもらう、といった流れを作ってみました。撮った画像をそのままCodexに渡す作りになっていますが、出来たらローカルのVLMを使って画像の説明部分はJetsonの中で閉じるようにしたいな、と思っています。

それにしてもJetsonとコーディングエージェントは面白い組み合わせだな、と思います。アイデア次第で色々なことが出来そうです!