TensorFlow.js と ONNX Runtime を使用した高性能画像分類アプリケーション。課題仕様準拠のシンプルモードと、7種類のモデルを選択できる拡張モードを提供します。
ダッシュボード画面 |
画像アップロード画面(デュアルモード対応) |
画像詳細画面(メタデータ表示) |
本リポジトリでは、TensorFlow.js と ONNX Runtime を組み合わせた画像分類アプリケーションを提供しています。
このアプリケーションは求職中だった ponponusa がソフトウェア企業への選考に進むにあたって技術課題として提示されたものへの解答となります。
実装目標の作業時間として累計16時間が目標、提出期限は2週間でした。
このアプリケーションはの実装にかかっている実質的な作業時間としては約20時間です。
基本機能の実装については、約14時間、renderへのデプロイで約4時間、クライアントでの推論対応で約2時間です。
実装には積極的に生成AIエージェントを活用し、コードの品質向上と効率化を図っています。
TensorFlow.js を用いたNode.jsアプリケーションを選択し、さらにONNX Runtimeも組み合わせることで、より多様なモデルを扱えるように拡張しています。
データベースには軽量でセットアップが容易なSQLiteを採用し、CLIとREST APIの両方を提供することで、様々な利用シーンに対応できるよう設計しました。
Web UIにはAstro + Svelteを採用し、モダンで高速なフロントエンドを実現しています。
TypeScriptを全体の言語として採用し、型安全性と開発効率の向上を図っています。
課題ではAIによるブラックボックスとなっているAPI部分を可能な限り独自に解釈し、Strategy Patternを用いて分析器を抽象化することで、将来的な拡張性を確保しつつ課題仕様も満たしながら、独自の機能を提供できるように工夫しています。
AIによるコーディングで懸念されがちな動くだけのコードではなく、AIエージェントをしっかりと活用しコードの品質向上と効率化を実現、ESLintやPrettierによるコード整形、Vitestによるユニットテスト(PlaywrightによるE2Eテストは準備のみ)を導入し、堅牢なコードベースを維持しドキュメントも充実させています。
docs/assignment-api.md に課題APIの詳細な実装内容、テスト内容、仕様との差異、トラブルシューティングを記載しています。合わせてご参照ください。
本番環境として公開されています:
Note:
- Free Tierは15分間非アクティブでスリープします。初回アクセス時は起動に30秒程度かかります。
- Free Tierではディスクストレージが利用できないため、データベースとアップロード画像は再デプロイ時にリセットされます。
- 永続化が必要な場合は Starter プラン($9/月)をご利用ください。
すべてのプラットフォーム(Windows/Mac/Linux)で同一の環境を提供します。
# リポジトリをクローン
git clone <repository-url>
cd image-classifier-imagenet
# Dockerコンテナをビルド・起動(初回は5-10分程度かかります)
docker compose up -d
# ブラウザで http://localhost:4321 を開く
# CLIコマンドをコンテナ内で実行
./docker-cli.sh register --image test-images/cat.jpg --analyzer mobilenet
./docker-cli.sh list
./docker-cli.sh classes
# または、直接docker composeコマンドを使用
docker compose exec app pnpm cli register --image test-images/cat.jpg
docker compose exec app pnpm test
| 項目 | 開発環境 | 本番環境 |
|---|---|---|
| Dockerファイル | Dockerfile |
Dockerfile.prod |
| Composeファイル | docker-compose.yml |
docker-compose.prod.yml |
| イメージ名 | image-classifier-dev |
image-classifier-prod |
| コンテナ名 | image-classifier-dev |
image-classifier-prod |
| イメージサイズ | 約5.2GB | 約5.2GB |
| 最適化 | ビルドツール保持 | ビルドツール削除・キャッシュクリア |
| ホットリロード | ✅ 対応(開発モード) | ❌ 非対応(プレビューモード) |
| デバッグツール | ✅ 含まれる | ✅ 含まれる |
| 起動モード | pnpm dev |
pnpm preview |
| 用途 | ローカル開発・テスト | 本番デプロイ・ステージング確認 |
| ボリューム | ホストマウント | 専用ボリューム(永続化) |
| データベース | ./db (ホスト) |
db-data ボリューム |
| アップロード画像 | ./public/uploads |
uploads-data ボリューム |
Note:
- 本番環境は開発環境と同じ依存関係を保持しつつ、ビルド後に不要なツール(Python、make、g++など)を削除してクリーンアップしています。
- 本番環境ではアップロードされた画像を専用ボリュームで管理し、
public/uploadsとdist/client/uploadsの両方にマウントすることで、Astroのビルド済み環境でも正しく配信されます。- データベースは
main.dbを使用し、環境変数DATABASE_PATHで指定されています。- データベース自動初期化: 両環境ともエントリーポイントスクリプト(
docker-entrypoint.sh)により、初回起動時に自動的にデータベースが作成されます。手動でのpnpm db:init実行は不要です。
プロジェクトには便利な起動スクリプトが用意されています:
# 開発環境を起動
./run-dev.sh # バックグラウンドで起動
./run-dev.sh --build # 再ビルドして起動
./run-dev.sh --no-detach # フォアグラウンドで起動(ログ表示)
# 本番環境を起動
./run-prod.sh # バックグラウンドで起動
./run-prod.sh --build # 再ビルドして起動
./run-prod.sh --no-detach # フォアグラウンドで起動(ログ表示)
# 環境のクリーンアップ
./docker-cleanup.sh # 開発環境のみクリーンアップ
./docker-cleanup.sh --target prod # 本番環境のみクリーンアップ
./docker-cleanup.sh --target both # 両方クリーンアップ
./docker-cleanup.sh --target dev -v # ボリュームも含めて削除
# コンテナをビルド・起動
docker compose up -d
# ビルドから実行
docker compose up -d --build
# ログを表示(リアルタイム)
docker compose logs -f
# コンテナを停止
docker compose down
# コンテナを完全削除(データベース・ボリューム含む)
docker compose down -v
# コンテナ内でシェルを実行
docker compose exec app sh
# データベースを再初期化(通常は不要、初回起動時に自動作成される)
docker compose exec app pnpm db:init
# AI分析ログにサンプルデータを挿入(課題モードのテスト用)
docker compose exec app pnpm db:seed-ai-log
# 本番イメージをビルド・起動
docker compose -f docker-compose.prod.yml up -d --build
# 起動のみ(イメージビルド済みの場合)
docker compose -f docker-compose.prod.yml up -d
# ログを表示
docker compose -f docker-compose.prod.yml logs -f
# コンテナを停止
docker compose -f docker-compose.prod.yml down
# コンテナを完全削除(データベース含む)
docker compose -f docker-compose.prod.yml down -v
# コンテナの状態確認
docker compose -f docker-compose.prod.yml ps
# イメージサイズを確認
docker images image-classifier
Docker を使用しない場合のセットアップ方法は、プラットフォーム別ドキュメントを参照してください:
注意: ローカル環境はネイティブモジュールのビルドが複雑なため、Docker環境を推奨します。
クライアント側で画像分析を実行する場合、ONNXモデルファイルが必要です:
# モデルファイルを自動ダウンロード
pnpm run download:models
詳細は クライアントモデルセットアップガイド を参照してください。
# 開発サーバーを起動
pnpm dev
# ブラウザで http://localhost:4321 を開く
主な機能:
Web UIでは2つのモードを切り替えられます:
課題モード 📋
/api/assignment/upload を使用拡張モード 🎛️
# 画像を登録(デフォルト: mobilenet)
pnpm cli register --image path/to/image.jpg
# 特定のアナライザーを指定して登録
pnpm cli register --image path/to/image.jpg --analyzer squeezenet-onnx
# 利用可能なアナライザー一覧を表示
pnpm cli analyzers
# 画像一覧を表示
pnpm cli list
# 特定画像の詳細を表示(メタデータ含む)
pnpm cli get <id>
# クラス統計を表示
pnpm cli classes
# 特定クラスの画像を表示
pnpm cli images-by-class <className>
# 画像を削除
pnpm cli delete <id>
# すべての画像を削除
pnpm cli delete-all --yes
詳細は docs/cli-manual.md を参照。
# APIサーバーを起動
pnpm dev
# APIエンドポイント: http://localhost:4321/api
エンドポイント:
POST /api/upload?analyzer=<type> - 画像をアップロード・分析(モデル選択可能)GET /api/images - 画像一覧を取得GET /api/images/:id - 特定画像の詳細を取得(メタデータ含む)DELETE /api/images/:id - 画像を削除GET /api/classes - クラス統計を取得GET /api/classes/:className/images - 特定クラスの画像一覧を取得GET /api/analyzers - 利用可能なアナライザー一覧を取得POST /api/assignment/upload - 課題仕様準拠の画像分析APIimage_path (FormData){success: boolean, message: string, estimated_data: {class: number, confidence: number}}ai_analysis_log テーブルに記録詳細は docs/openapi.yaml を参照。
本プロジェクトは、Strategy Patternを採用した拡張可能な分析器アーキテクチャを実装しています。
ワーカースレッドによるC++メモリ解放
ONNX RuntimeやTensorFlow.jsはC++ネイティブモジュールを使用しており、Node.jsのガベージコレクション(GC)では解放できないメモリを確保します。本プロジェクトでは、以下の手法でメモリを確実に解放:
worker.terminate() で強制終了// src/lib/worker-helper.ts
const worker = new Worker(workerPath, { workerData: { imagePath, analyzerType } });
worker.on('message', (result) => {
worker.terminate(); // メモリ解放のため即座に終了
resolve(result.data);
});
ビルドプロセス
tsx でTypeScriptを直接実行esbuild で事前ビルド(dist/worker/image-analyzer-worker.js)MockAnalyzer(開発/テスト用)
MobileNet V3 Analyzer
MobileNet V2 Analyzer
Inception V3 Analyzer
SqueezeNet ONNX Analyzer
MobileNet V2 ONNX Analyzer
ResNet-50 ONNX Analyzer
詳細は docs/analyzer-architecture.md を参照。
# 本番ビルド
pnpm build
# ビルド結果のプレビュー
pnpm preview
# すべてのテストを実行
pnpm test
# ウォッチモード
pnpm test:watch
# カバレッジ
pnpm test:coverage
# E2Eテスト(未実装)
pnpm test:e2e
テスト構成:
# Linter
pnpm lint
# Formatter(チェック)
pnpm format:check
# Formatter(自動修正)
pnpm format
image-classifier-imagenet/
├── db/ # データベースファイル
│ ├── schema.sql # SQLスキーマ定義
│ └── main.db # SQLiteデータベース
├── docs/ # ドキュメント
│ ├── analyzer-architecture.md # 分析器アーキテクチャ
│ ├── cli-manual.md # CLIマニュアル
│ └── openapi.yaml # API仕様
├── models/ # ONNXモデル(自動ダウンロード)
│ ├── squeezenet1.1-7.onnx
│ ├── mobilenetv2-7.onnx
│ └── resnet50-v1-7.onnx
├── public/ # 静的ファイル
│ ├── images/ # 静的画像
│ └── uploads/ # アップロードされた画像
├── src/
│ ├── components/ # Svelteコンポーネント
│ │ ├── ImageUploadModal.svelte # モード切り替え対応
│ │ ├── ImageList.svelte
│ │ └── ClassStats.svelte
│ ├── layouts/ # Astroレイアウト
│ ├── lib/ # ライブラリコード
│ │ ├── analyzers/ # 画像分析器
│ │ │ ├── base-analyzer.ts
│ │ │ ├── onnx-base-analyzer.ts
│ │ │ ├── mock-analyzer.ts
│ │ │ ├── mobilenet-analyzer.ts
│ │ │ ├── mobilenet-v2-analyzer.ts
│ │ │ ├── inception-v3-analyzer.ts
│ │ │ ├── squeezenet-onnx-analyzer.ts
│ │ │ ├── mobilenet-v2-onnx-analyzer.ts
│ │ │ ├── resnet50-onnx-analyzer.ts
│ │ │ ├── imagenet-labels.json
│ │ │ └── factory.ts
│ │ ├── types/
│ │ │ └── database.ts # DB型定義(ai_analysis_log含む)
│ │ ├── api-client.ts # APIクライアント
│ │ ├── database.ts # データベース接続
│ │ ├── repository.ts # データアクセス層
│ │ └── utils/ # ユーティリティ
│ ├── pages/ # Astroページ
│ │ ├── api/ # APIエンドポイント
│ │ │ ├── upload.ts # Extended API
│ │ │ ├── analyzers.ts # アナライザー一覧
│ │ │ └── assignment/
│ │ │ └── upload.ts # Assignment API
│ │ └── *.astro # Webページ
│ ├── scripts/ # CLIスクリプト
│ │ ├── cli.ts # CLIエントリーポイント
│ │ └── commands/ # CLIコマンド実装
│ │ ├── analyzers.ts # アナライザー一覧
│ │ └── register.ts # -a オプション対応
│ └── styles/ # スタイルシート
├── tests/ # テストコード
│ ├── unit/ # ユニットテスト
│ │ ├── assignment-api.test.ts # Assignment APIテスト
│ │ └── ...
│ ├── e2e/ # E2Eテスト
│ └── fixtures/ # テストフィクスチャ
└── test-images/ # テスト用画像
| 分析器 | ランタイム | 初期化 | 1画像 | 10画像 | メモリ |
|---|---|---|---|---|---|
| Mock | - | <1ms | <1ms | 5ms | <1MB |
| MobileNet V3 | TensorFlow | 3.2s | 250ms | 2.5s | 50-100MB |
| MobileNet V2 | TensorFlow | 2.8s | 200ms | 2.0s | 40-80MB |
| Inception V3 | TensorFlow | 4.5s | 400ms | 4.0s | 100-150MB |
| SqueezeNet | ONNX Runtime | 1.0s | 100ms | 1.0s | 5-10MB |
| MobileNet V2 (ONNX) | ONNX Runtime | 1.5s | 150ms | 1.5s | 15-30MB |
| ResNet-50 (ONNX) | ONNX Runtime | 2.5s | 700ms | 7.0s | 100-200MB |
本番環境(docker-compose.prod.yml)でアップロードした画像が表示されない場合、ボリューム設定を確認してください。
原因: Astroのビルド済み環境では静的ファイルがdist/client/から配信されるため、実行時にアップロードされた画像が見つからない。
解決策: uploads-dataボリュームをpublic/uploadsとdist/client/uploadsの両方にマウント(v2.0以降で対応済み)
volumes:
- uploads-data:/app/public/uploads
- uploads-data:/app/dist/client/uploads
/api/classesが{"classes":[]}を返す場合、以下を確認してください:
データベースパスの確認: 環境変数DATABASE_PATHが正しく設定されているか
# docker-compose.prod.yml
environment:
- DATABASE_PATH=/app/db/main.db # main.db を使用
APIエンドポイントの動的レンダリング: prerender = falseが設定されているか
// src/pages/api/classes.ts
export const prerender = false;
データベース自動初期化の確認: 初回起動時にログを確認
# 起動ログでDB初期化メッセージを確認
docker compose -f docker-compose.prod.yml logs app | grep -i database
# 出力例:
# Database not found at /app/db/main.db. Initializing database...
# Database initialized successfully.
Note: 通常は自動初期化されるため手動でのpnpm db:init実行は不要です。問題がある場合のみ手動で実行してください。
CLIコマンドの出力が文字化けする場合、UTF-8エンコーディングを設定してください:
一時的な対処(現在のセッションのみ):
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$env:PYTHONIOENCODING = "utf-8"
chcp 65001
恒久的な対処(推奨):
PowerShellプロファイルに追加:
# プロファイルを開く
notepad $PROFILE
# 以下を追加して保存
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
または、Windows Terminalを使用すると自動的にUTF-8が適用されます(推奨)。
# ネイティブモジュールの再ビルド
npm rebuild @tensorflow/tfjs-node --build-from-source
# Node.jsヒープサイズを増やす
export NODE_OPTIONS="--max-old-space-size=4096"
# または軽量なMockAnalyzerを使用
export ANALYZER_TYPE=mock
MobileNetの設定を調整:
// src/lib/analyzers/mobilenet-analyzer.ts
this.model = await mobilenet.load({
version: 1, // version 2より高速
alpha: 0.5, // モデルサイズを削減
});
MIT
プルリクエストを歓迎します。大きな変更の場合は、まずissueを開いて変更内容を議論してください。