できるまで

QRコード読み取りアプリ ができるまで

ユーザーと Claude(AI)の対話だけで、ゼロからフル機能アプリが完成するまでの全記録

1日
開発期間
80+
やりとりの回数
8
ファイル数
4言語
PHP/JS/Python/CSS

その日、ユーザーはWindows PCに Claude Code(AnthropicのAI搭載CLIツール)をインストールしたばかりでした。AIがコードを書いてくれるらしい、という話は聞いていたものの、ドキュメントも読まず、チュートリアルもやらず、とりあえず何か作ってみようと思い立ったのがこのアプリの始まりです。

前情報ゼロ、手探りの状態から「QRコードの読み取りアプリを作りたい」とだけ伝えて開発スタート。コードは1行も手書きしていません。すべてClaudeが生成し、ユーザーがブラウザで動作確認してフィードバックを返す、というサイクルで開発されました。

使用技術スタック

PHP (Apache/XAMPP) JavaScript Python 3 ZXing jsQR OpenCV pyzbar html5-qrcode BarcodeDetector API Web Audio API PIL/Pillow

ファイル構成

C:\xampp\htdocs\qrcode\ ... Web公開ディレクトリ
├── index.php ... メインアプリ(HTML/CSS/JS + PHPテンプレート)
├── api.php ... 履歴CRUD API(保存/取得/クリア)
├── scan.php ... Python呼び出しブリッジ
├── scan_qr.py ... サーバーサイドQR検出エンジン
└── scan_test.py ... デバッグ用テストスクリプト

C:\xampp\****\ ... htdocs外(Web非公開)
├── ****/ ... ブラウザ別履歴JSONファイル(3時間で自動削除)
└── ****/ ... スキャンログ

開発タイムライン

1
「QRコードの読取アプリを作りたい」

すべてはこの一言から始まりました。

ユーザー
Apache+PHPでQRコードの読込アプリを作りたい。XAMPPをインストールしたいので手伝ってほしい。
Claude
XAMPPのインストールからお手伝いします。作業ディレクトリは C:\xampp\htdocs\qrcode\ でよろしいですか?
ユーザー
これからインストールします。作業ディレクトリはそうするつもりです。

XAMPPのインストールが完了した後、Claudeが基本アプリを一気に生成。カメラ映像からjsQRでリアルタイム読み取り、画像アップロードでhtml5-qrcodeによるスキャン、PHPで履歴の保存/読み出し/ログ記録までの一通りの機能が最初の数回のやりとりで動き始めました。

ポイント: 最初のプロトタイプは、index.php(フロントエンド)+ api.php(バックエンドAPI)の2ファイル構成。 わずか数分で「カメラで読む」「ファイルで読む」「履歴を保存する」が動く状態に。
機能実装
2
白QRコードが読めない!

基本機能が動いた直後、最初の壁にぶつかりました。

ユーザー
オレンジ背景に白いQRコードが読み取れていない。また、1枚の画像に複数のQRコードがある場合、全て履歴に吐き出したい

通常のQRコードリーダーは「白い背景に黒いモジュール」を前提に設計されています。オレンジのグラデーション背景に白いモジュールのQRは、まったく検出できませんでした。

試行1: 単純なピクセル反転

まず試したのは、画像の全ピクセルの色を反転する方法。白→黒、オレンジ→青に変わります。しかしオレンジのグラデーション背景は反転しても均一な白にはならず、検出率は上がりませんでした。

試行2: 彩度フィルター(これが正解)

色の「鮮やかさ(彩度)」に注目するアイデアを発見。オレンジは彩度が高く、白は彩度がほぼゼロ。この差を利用して:

// 彩度フィルターの核心ロジック for (pixel in image) { sat = max(R,G,B) - min(R,G,B); // 彩度を計算 if (sat > 50) → 白 // カラー背景 → 白に変換 else → 明るければ黒、暗ければ白 // 白QR → 黒に変換 }
Before オレンジ背景+白QR → 検出不可
After(彩度フィルター適用後) 白背景+黒QR → 標準的なQRとして検出成功!
問題解決
3
「5個あるのに1個しか取れない」- 複数QR検出の壁

テスト画像には5個のQRコードが含まれていました。しかし検出できるのは常に1個だけ。

ユーザー
やっぱり一個しか検出しません。画像は動作時の警告です
Claude
jsQR / ZXingは1回の呼び出しで1つのQRしか返しません。複数QR検出には別のアプローチが必要です。

アプローチ1: BarcodeDetector API

Chrome/Edgeに搭載されたネイティブAPI。複数QRを一度に返せる唯一のブラウザAPI。しかし対応ブラウザが限られ、検出精度も不安定でした。

アプローチ2: jsQRマスクループ

QRを検出したら、その領域を白で塗りつぶして再スキャン。これを最大10回繰り返す方法。

// 検出 → マスク → 再検出ループ for (i = 0; i < 10; i++) { code = jsQR(imageData); if (!code) break; results.push(code.data); maskRegion(imageData, code.location); // 検出エリアを白塗り }

アプローチ3: グリッド分割

画像を2x2、3x3、4x4のグリッドに分割し、各セルを個別にスキャン。セル間は20%オーバーラップさせて境界上のQRも取りこぼさない設計。

これらすべてを組み合わせて 4件まで到達。しかし5件目(App StoreのQR)がどうしても取れません。

ユーザー
さっきの画像で4件まで取れたっぽい
ユーザー
改善を続けてほしい
ユーザー
うーん、まだ同じく4件だけだなぁ......

コントラスト強調、閾値の変更、グリッドパターンの追加(2x3、3x2)... あらゆる手を尽くしましたが、JavaScript のQRライブラリではこの5件目のQRを認識できませんでした。

問題解決
4
Python投入 → ついに全件検出!

JavaScript単体での限界を感じたユーザーから、重要な提案がありました。

ユーザー
うーん、まだだめだね。LinuxやWindowsで使える、画像を送ったらQRのURLを連番で出してくれるソフトウェアとかないかな?
Claude
Python の pyzbar + OpenCV の組み合わせが強力です。PHPから exec() で呼び出す形で統合できます。
ユーザー
やってみようか

まず scan_test.py(テスト用)を作成して手元で検証。結果は劇的でした。

# scan_test.py の実行結果 --- 全体スキャン --- [全体 通常] {'https://example1.com', 'https://example2.com'} [全体 彩度フィルター(50)] {'https://example3.com'} --- グリッド 2x2 --- [2x2[1,0] 通常] {'https://example4.com'} [2x2[0,1] 反転] {'https://example5.com'} 検出結果 計5件 ← !!!

5件すべて検出成功! pyzbar の認識力は jsQR/ZXing を大きく上回っていました。

しかし最初の統合ではうまくいきませんでした。PHPから Python を呼び出して結果を受け取る部分でつまずきます。

ユーザー
うーん、まだ4件だね。pythonが失敗したらそのまま返したほうがいいんじゃないだろうか

Python が失敗した場合にJSフォールバックに切り替わっていたため、常に4件(JSの限界)で止まっていたのです。scan.php のエラーハンドリングを修正し、Python の出力をJSON形式で正しくパースするよう改善。

// scan.php: PHPからPythonを呼ぶブリッジ $cmd = "{$python} {$scriptPath} {$imagePath} 2>NUL"; exec($cmd, $output, $returnCode); $raw = implode('', $output); $data = json_decode($raw, true);
Windows固有の罠: 最初は 2>/dev/null と書いていてWindowsで動かなかった。2>NUL に修正する必要がありました。
ユーザー
いけたね、ありがとう!
機能実装
5
アノテーション画像の生成

QRコードが検出された後、「どこにあったか」が視覚的にわかるアノテーション画像を生成する機能を追加。

ユーザー
完了後にどのQRが取れたか画像を出せるとかっこいいかも

Python の PIL(Pillow)で元画像の上にカラー枠と番号バッジを描画。検出されたQRの位置を色分けしたポリゴンで囲み、丸い番号バッジを付けます。

# アノテーション描画(scan_qr.py) scale = max(pil_img.size) / 1000 line_w = max(6, int(8 * scale)) # 枠線の太さ badge_r = max(16, int(20 * scale)) # バッジの半径 # 半透明の色付きポリゴン + 白い縁取り + カラー枠線 draw.polygon(poly, fill=color_rgb + (60,)) draw.line([...], fill=(255,255,255,220), width=line_w+4) draw.line([...], fill=color_rgb+(255,), width=line_w) # 番号バッジ(白縁の丸 + 番号テキスト) draw.ellipse([...], fill=color_rgb+(255,)) draw.text((cx, cy), str(i+1), fill='white', font=font)

ここからUIの微調整が始まります。

ユーザー
枠線とナンバーをもっと太く目立つようにしてほしい
ユーザー
数字はさっきの大きさとのちょうど中間くらいがいいかも
ユーザー
フォントサイズはそのままで、丸のサイズはもうちょっと小さく

3回のやりとりで、バッジのサイズ感がちょうど良く調整されました。生成された画像はBase64エンコードでJSONに埋め込んでフロントに返し、クリックで全画面プレビューできるようにしました。

機能実装 UI改善
6
スキャンが遅すぎる問題

iPhoneで撮影した写真(4032x3024ピクセル)をアップロードすると、処理に1分以上かかっていました。前処理バリアント7種 × グリッドパターン5種 × 全ピクセル操作が原因でした。

Before 7種のバリアント × 5種のグリッド
原寸(4032x3024)で処理
所要時間: 60秒以上
After 3種のバリアント × 段階的グリッド
最大2000pxにリサイズ
所要時間: 数秒

最適化のポイント:

  • 画像を最大2000pxにリサイズしてから処理
  • 前処理バリアントを7種→3種(通常・反転・彩度フィルター)に厳選
  • グリッドを段階的に実行: まず全体スキャン → 2x2で新しいQRが見つかれば3x3へ → 3x3で見つかれば4x4へ。見つからなければそこで打ち切り
# 段階的グリッドエスカレーション(scan_qr.py) all_results.update(scan_image(pil_img)) # 全体スキャン new = scan_with_grid(pil_img, 2, 2, all_results) all_results.update(new) if new: # 2x2で新規発見があった場合のみ3x3へ new2 = scan_with_grid(pil_img, 3, 3, all_results) all_results.update(new2) if new2: # 3x3でも新規発見があれば4x4へ all_results.update(scan_with_grid(pil_img, 4, 4, all_results))
問題解決
7
履歴機能の充実とブラウザ分離

スキャン機能が安定した後、履歴まわりの機能が次々とリクエストされました。

ユーザー
スキャン履歴にIPは表示しなくていいかな。あと、同じURLの時は読み取り時間を列記するだけでいい気がする
ユーザー
重複URLの時間は1行一件にして改行入れて。あと時間は右寄せがいいかな。

重複URLのグルーピング表示と時刻の右寄せレイアウトを実装。

ユーザー
メールで送信を押してもGmail開かない
ユーザー
Gmailない人はどうしよう
ユーザー
ついでにクリップボードにコピーも入れようか

Gmail送信(Gmailの作成画面URL)、標準メーラー送信(mailto:)、クリップボードコピーの3つのアクションボタンが追加されました。

ユーザー
常に3つが画面いっぱいに並ぶようにしたい。文字オーバーする場合はアイコンで分かりやすく
ユーザー
切り分け680くらいがいいかも

レスポンシブ対応で、680px以下の画面幅ではフルテキストの代わりに短縮ラベル(Gmail / Mailer / Copy)+ アイコンで表示。

ブラウザごとの履歴分離

ユーザー
同じLANの中にいるスマホで確認したいんだけど、URLわかる?
Claude
http://***.***.***.***//qrcode/ でアクセスできます。
ユーザー
みれた!スキャン履歴はブラウザごとに別々に管理したい

localStorageにUUIDを保存し、APIリクエストに client_id として付与する仕組みを実装。サーバー側では data/history_{client_id}.json のように分離保存。同じPCでもChromeとEdgeで履歴が別になります。

// クライアントID(ブラウザ識別) function getClientId() { let id = localStorage.getItem('qr_client_id'); if (!id) { id = crypto.randomUUID(); localStorage.setItem('qr_client_id', id); } return id; }
機能実装
8
UIをタブスタイルに刷新

「カメラ」と「画像アップロード」の切り替えが分かりにくいという問題を解決。

ユーザー
カメラで読み取り と 画像をアップロード がボタンだとわかりにくいので、タブで分ける感じにして、今どっちのモードかわかりやすくしてほしい
ユーザー
デザインは写真みたいな感じでどうかな?(ブラウザタブの参考画像を送付)

ブラウザのタブバーを模したデザインに変更。アクティブタブは白背景で上部に青いボーダー、非アクティブはグレー背景。パネルの青い枠線がタブに直接つながるように border-bottom: 2px solid white + margin-bottom: -2px で重ね合わせ。

ユーザー
カメラで→カメラでQR読取 / 画像で→画像からQR検出
ユーザー
あとタブとウインドウの青い境界線かっこ悪いので続いてるようにしてほしい

タブのラベル名も何度か微調整して、最終的に「カメラでQR読取」「画像からQR検出」に落ち着きました。

UI改善
9
カメラ機能をZXingベースに再構築

Claude Codeをインストールした直後、チュートリアル代わりに約2時間かけて作った別プロジェクトのQRリーダーがありました。そのカメラスキャンコードを今回のアプリに移植します。

ユーザー
カメラでQR読取は、以前作成した「c:\claude\qrreader」のソースをもとに再構築して

旧アプリのjsQRベーススキャンから、ZXingベースに全面書き換え。大きく進化したポイント:

Before(jsQR) 全画面をスキャン
200ms間隔のsetInterval
手動の反転モード切替が必要
カメラ選択不可
After(ZXing) 中央60%をクロップ+フレーム表示
requestAnimationFrame + 500ms間隔
自動反転フォールバック
カメラ切替ドロップダウン
ビープ音フィードバック
グレースケール+コントラスト強調

スキャンフレーム(赤い角マーク付き)は、中央60%の領域を示し、周囲を暗くすることでQRコードを合わせやすくなりました。白QRコードも自動で色反転して検出するため、「反転モード」ボタンが不要に。

ユーザー
PCでvideo-wrapperの縦サイズが大きすぎる気がする
ユーザー
そうなると内側の枠がはみ出ちゃってるね。枠を小さくしてあげて
ユーザー
いけたね

PCでは max-height: 360px に制限し、スキャンフレームを height: 80% + aspect-ratio: 1 で常にビデオ内に収まるよう調整。

機能実装 UI改善
10
メニュー・ヘルプページの追加

仕上げとして、ヘッダーに3点メニューを追加。

ユーザー
上の青いバーの右上にメニューアイコンを作成し、クリックすると「使い方」「できるまで」を作成

「使い方」は各機能の使い方をわかりやすく説明するページ。「できるまで」はこのページ ── 開発の全過程をダイジェストとしてまとめたものです。

UI改善
11
セキュリティ強化と自動クリーンアップ

運用面での改善が2つ加わりました。

ユーザー
ユーザーごとの履歴だけど、ユーザーがクリアするまで残ったままなのかな?もしそうなら数時間でクリアしたい。
ユーザー
dataとlogsはhtdocsの中にあると流出の危険性があるので、htdocsの一個上に持って行って

3時間自動有効期限

履歴の取得時に3時間以上前のエントリを除外し、APIアクセスのたびに更新のない古い履歴ファイル自体も削除する仕組みを追加。一時的な読み取り用途のアプリなので、データが自動で消えるのは理にかなっています。

// api.php: 古い履歴ファイルを自動削除 function cleanupOldFiles($dir, $maxAge) { $now = time(); foreach (glob($dir . '/history_*.json') as $file) { if ($now - filemtime($file) > $maxAge) { unlink($file); } } }

データディレクトリをhtdocs外に移動

履歴JSONやスキャンログがWeb公開ディレクトリ内にあると、URLを直接叩けばアクセスできてしまいます。htdocsの外(C:\xampp\****\)に移動して、PHPからのみアクセス可能にしました。

Before C:\xampp\htdocs\qrcode\data\
C:\xampp\htdocs\qrcode\logs\
→ URLで直接アクセス可能
After C:\xampp\****\data\
C:\xampp\****\logs\
→ PHP経由でのみアクセス可能
機能実装 問題解決
12
Ubuntu VPSへの移植と本番公開

ローカルのXAMPP環境で完成したアプリを、インターネットに公開することに。

ユーザー
ubuntuのVPS借りるので、そこに移植したい
Claude
セットアップスクリプトを作成します。Apache + PHP + Python環境の構築からファイル転送まで一括でやります。

VPSにはWebサーバーもPHPも入っていない素の状態。Claudeがセットアップスクリプト(setup_ubuntu.sh)とデプロイスクリプト(deploy.sh)を生成し、SSH経由でリモート実行。

途中、いくつかの壁にぶつかりました:

  • Ubuntu 24.04でパッケージ名が変更(libgl1-mesa-glx → libgl1)
  • PHPのアップロード上限が2MB → 画像が送れない → 20MBに変更
  • php-mbstring が未インストール → メタ情報取得が500エラー
  • scan.php の 2>NUL(Windows) → OS自動判定に修正

Cloudflareでドメイン(kjm3.tech)を設定し、SSL/TLSをFlexibleモードに。HTTPSで全機能が動作するようになりました。

ポイント: HTTPSがないとブラウザの getUserMedia(カメラAPI)が使えない。Cloudflareの無料SSLで解決。
機能実装 問題解決
13
小さいQRの検出強化と処理タイムアウト

7つのQRコードがある画像で3つしか検出できない問題が発覚。

ユーザー
この画像だとQR7つあるのに3つしか取れてないっぽい

各セクションの隅に配置された小さなQRコードが、グリッド分割しても解像度不足で検出できていませんでした。

Before 最大2000px / 条件付きグリッド拡大
オーバーラップ20% / 3件検出
After 最大3000px / 常に5x5まで実行
オーバーラップ25% / セル自動拡大
シャープ化バリアント追加 / 7件全検出

さらに、大きな画像の処理に時間がかかりすぎる問題に対して、10秒のタイムアウトを実装。時間内に見つかったQRコードだけを返し、「処理時間超過のため途中結果です」と表示します。

# scan_qr.py: タイムアウト制御 TIMEOUT = 10 # 秒 def is_timeout(): return (time.time() - _timer[0]) >= TIMEOUT # グリッドの各段階でチェック for grid in [(2,2), (3,3), (4,4), (5,5)]: if is_timeout(): break new = scan_with_grid(pil_img, grid, all_results)
ユーザー
ぜんぶとれた!いいね!
機能実装 問題解決
14
URL概要の自動取得とサイト全体の仕上げ

読み取ったQRコードがURLだった場合、そのサイトのメタ情報(description)を自動取得する機能を追加。

ユーザー
QRの結果がURLだった場合、そのサイトの概要とかって取ってきたりできますか?
Claude
できます。URLのメタ情報(タイトル、説明文、OG画像)を取得してリンクプレビューを表示する機能を作りましょう。

PHPでURLのHTMLを取得し、og:description や meta description をパースして履歴データに保存。スキャン履歴のURL下に概要文が表示されるようになりました。メール送信・コピー時にも概要が含まれます。

仕上げとして以下も追加:

  • ファビコン(QRパターンのSVGアイコン)
  • 全ページOGP対応(SNS共有時のプレビューカード)
  • コピーライト表示
  • スマホはカメラモード、PCは画像アップロードをデフォルトに
  • メニューにTOPページへのリンク追加
機能実装 UI改善

ふりかえり

このアプリは、ユーザーと Claude(AI)の対話だけで、環境構築から機能実装・デバッグ・UI調整、さらにVPSへの本番デプロイまですべてを1日で完成させました。

最大の壁: 複数QRコードの全件検出

開発時間の大半を費やしたのがこの問題です。jsQR、ZXing、BarcodeDetector、マスクループ、グリッド分割... JavaScriptだけで5種類以上のアプローチを試しましたが、5件中4件が限界。最終的に Python(pyzbar + OpenCV)をサーバーサイドに導入することで突破できました。

白いQRコードという盲点

「QRコードは黒い」という常識を覆す白QRコード。単純な反転では解決できず、「彩度」という画像処理の概念を使った独自フィルターが鍵でした。

UI は「一発OK」にならない

アノテーションのバッジサイズ、タブのデザイン、ボタンのレスポンシブ対応... UIの調整は何度もスクリーンショットを見ながら微調整する対話が必要でした。「もうちょっと小さく」「中間くらい」「680くらいがいいかも」── このやりとりこそが、AIとの共同開発の醍醐味かもしれません。

AIとの開発で学んだこと

開発者より

Claude codeはじめてちゃんとさわってみたけどなんこれすごい。

ブラウザとJS任せで画像からQRコード読んでもうまくとれなかったので、外部のプログラムで処理してよとか、VPSが安いやつなんで処理途中で切ってみてよとか無茶ぶってもすぐ対応。
適当に借りたVPSへのアップまでほぼノータッチでやってくれちゃったりしました。

途中処理が重くなったり、会話履歴忘れられたりとかしてましたが、そこらへんはこれから勉強していけばなんとか対応できそうよね。

何より、ほんとコード一回も書いてないのにできちゃったのがすごい通り越して怖い。

今日の11時ごろに思い立ってClaude code入れて、なんかQR読み取り機作らせてみたらこれそのまま最後まで行けんじゃね?と思ってから現在20時ちょうど、昼飯・休憩で2時間引いても7時間でできるんだね。

すごい時代になったもんだ。

2026/3/8 KJM3

Built with Claude Code (claude-opus-4-6) + XAMPP + Python
Deployed on Ubuntu VPS + Cloudflare
2024-2026 - Made through human-AI collaboration