Fumiya Imazato Claude Opus 4.5 commited on
Commit
1eff75e
·
1 Parent(s): 251cf10

Fix: PaddleOCR 3.x use_gpu + overlay camera switch + auto analyze

Browse files

- Remove use_gpu arg (deprecated in PaddleOCR 3.x)
- Add camera switch button as overlay on video
- Enable streaming=True for auto analysis
- Use webcam.stream() instead of manual button
- Update TROUBLESHOOTING.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (3) hide show
  1. TROUBLESHOOTING.md +50 -0
  2. app.py +106 -118
  3. core/ocr_engine.py +7 -5
TROUBLESHOOTING.md CHANGED
@@ -323,3 +323,53 @@ const initRearCamera = async () => {
323
  };
324
  initRearCamera();
325
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  };
324
  initRearCamera();
325
  ```
326
+
327
+ ## PaddleOCR 3.x で use_gpu が廃止
328
+
329
+ ### 問題
330
+ ```
331
+ ValueError: Unknown argument: use_gpu
332
+ ```
333
+
334
+ ### 原因
335
+ - PaddleOCR 3.x で `use_gpu` 引数も廃止された
336
+ - `show_log`, `enable_mkldnn`, `cpu_threads` に続いて `use_gpu` も削除
337
+
338
+ ### 解決策
339
+ ```python
340
+ # 修正前
341
+ self._ocr = PaddleOCR(
342
+ use_angle_cls=True,
343
+ lang=self.lang,
344
+ use_gpu=self.use_gpu,
345
+ )
346
+
347
+ # 修正後(PaddleOCR 3.x)
348
+ self._ocr = PaddleOCR(
349
+ use_angle_cls=True,
350
+ lang=self.lang,
351
+ )
352
+ ```
353
+
354
+ ### 教訓
355
+ PaddleOCR 3.x では以下の引数のみ使用:
356
+ - `use_angle_cls`
357
+ - `lang`
358
+
359
+ ## カメラ切り替えボタンをオーバーレイ表示
360
+
361
+ ### 問題
362
+ - カメラ切り替えボタンが撮影画面の外にあり使いづらい
363
+
364
+ ### 解決策
365
+ JavaScriptでボタンを動的に追加:
366
+ ```javascript
367
+ const btn = document.createElement('button');
368
+ btn.className = 'camera-switch-overlay';
369
+ btn.innerHTML = '🔄 内/外';
370
+ btn.style.cssText = 'position:absolute;top:10px;right:10px;z-index:1000;...';
371
+
372
+ imageContainer.appendChild(btn);
373
+ ```
374
+
375
+ MutationObserverでDOMの変更を監視し、ボタンが消えたら再追加。
app.py CHANGED
@@ -6,7 +6,6 @@ GPSに頼らず位置を特定するサービス
6
  """
7
 
8
  import time
9
- import asyncio
10
  from typing import Optional
11
  import numpy as np
12
  import gradio as gr
@@ -47,17 +46,7 @@ class DokoCameApp:
47
  self._hint_lon: float = 139.7671
48
 
49
  def process_frame(self, frame: np.ndarray) -> dict:
50
- """
51
- フレームを処理
52
-
53
- Returns:
54
- {
55
- "ocr_texts": [...],
56
- "landmarks": [...],
57
- "location_status": "...",
58
- "result": AggregatedResult or None
59
- }
60
- """
61
  if frame is None:
62
  return self._empty_result()
63
 
@@ -74,17 +63,21 @@ class DokoCameApp:
74
 
75
  # OCR処理
76
  if sample.should_ocr:
 
77
  raw_texts = self.ocr_engine.detect_text_only(frame)
78
  ocr_texts = [clean_ocr_text(t) for t in raw_texts if t]
79
  self._latest_ocr_texts = ocr_texts
 
80
 
81
  # VLM処理
82
  if sample.should_vlm and self.vlm_analyzer.is_available:
83
  try:
 
84
  analysis = self.vlm_analyzer.analyze(frame)
85
  if analysis.success:
86
  self._latest_analysis = analysis
87
  vlm_keywords = self.vlm_analyzer.get_search_keywords(analysis)
 
88
  except Exception as e:
89
  print(f"VLM error: {e}")
90
 
@@ -138,27 +131,18 @@ class DokoCameApp:
138
  self._latest_ocr_texts = []
139
  self._latest_analysis = None
140
 
141
- def set_hint_location(self, lat: float, lon: float):
142
- """ヒント座標を設定"""
143
- self._hint_lat = lat
144
- self._hint_lon = lon
145
-
146
 
147
  # グローバルアプリインスタンス
148
  app = DokoCameApp()
149
 
150
 
151
  def process_webcam(frame):
152
- """Webcam入力を処理(Gradio Image入力用)"""
153
- print(f"[DEBUG] process_webcam called, frame type: {type(frame)}")
154
-
155
  if frame is None:
156
- print("[DEBUG] frame is None")
157
- return None, "カメラを起動してください", ""
158
 
159
- print(f"[DEBUG] frame shape: {frame.shape if hasattr(frame, 'shape') else 'no shape'}")
160
  result = app.process_frame(frame)
161
- print(f"[DEBUG] process_frame result: {result}")
162
 
163
  # OCRテキストをフォーマット
164
  ocr_display = ""
@@ -183,47 +167,102 @@ def process_webcam(frame):
183
  location_display += f"\n\n座標: {r.estimated_lat:.6f}, {r.estimated_lon:.6f}"
184
  location_display += f"\n信頼度: {r.confidence:.1%}"
185
 
186
- return frame, location_display, info_display
187
 
188
 
189
  def reset_state():
190
  """状態リセット"""
191
  app.reset()
192
- return "リセットしました"
193
 
194
 
195
  def create_ui():
196
  """Gradio UIを作成"""
197
 
198
- # ページ読み込み時に外カメラをデフォルトにするJavaScript
199
- init_camera_js = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  () => {
201
- // Gradioのカメラが起動したら外カメラに切り替える
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  const initRearCamera = async () => {
203
- // 少し待ってからカメラを探す
204
  await new Promise(r => setTimeout(r, 2000));
205
 
206
  const video = document.querySelector('video');
207
- if (!video) {
208
- console.log('Video element not found, retrying...');
209
- setTimeout(initRearCamera, 1000);
210
- return;
211
- }
212
-
213
- // カメラが起動するまで待つ
214
- if (!video.srcObject) {
215
- console.log('Camera not started yet, retrying...');
216
  setTimeout(initRearCamera, 1000);
217
  return;
218
  }
219
 
220
  try {
221
  const tracks = video.srcObject.getVideoTracks();
222
- if (tracks.length > 0) {
223
- tracks.forEach(track => track.stop());
224
- }
225
 
226
- // 外カメラを要求
227
  const stream = await navigator.mediaDevices.getUserMedia({
228
  video: { facingMode: { ideal: 'environment' } }
229
  });
@@ -234,39 +273,43 @@ def create_ui():
234
  }
235
  };
236
 
237
- // ページ読み込み後に実行
238
  initRearCamera();
 
 
 
 
 
 
 
 
239
  }
240
  """
241
 
242
  with gr.Blocks(
243
  title="どこカメ - リアルタイム位置特定",
244
  theme=gr.themes.Soft(),
245
- js=init_camera_js,
 
246
  ) as demo:
247
  gr.Markdown(
248
  """
249
  # 📍 どこカメ (dokoCame)
250
- ### かざすだけで、視界がそのまま住所になる
251
  """
252
  )
253
 
254
  with gr.Row():
255
  with gr.Column(scale=2):
256
- # カメラ入力(streaming無効にして手動キャプチャ)
257
  webcam = gr.Image(
258
  sources=["webcam"],
259
- streaming=False,
260
- label="📷 カメラで撮影してから解析ボタンを押してください",
261
  mirror_webcam=False,
 
262
  )
263
 
264
- with gr.Row():
265
- # 解析ボタン
266
- analyze_btn = gr.Button("🔍 解析する", variant="primary", size="lg")
267
- # カメラ切り替えボタン
268
- switch_camera_btn = gr.Button("🔄 内/外カメラ切替", variant="secondary")
269
-
270
  with gr.Column(scale=1):
271
  # 位置情報表示
272
  location_output = gr.Textbox(
@@ -278,91 +321,36 @@ def create_ui():
278
  # 検出情報表示
279
  info_output = gr.Textbox(
280
  label="🔍 検出情報",
281
- lines=10,
282
  interactive=False,
283
  )
284
 
285
  # リセットボタン
286
  reset_btn = gr.Button("🔄 リセット", variant="secondary")
287
 
288
- # カメラ切り替え用JavaScript
289
- switch_camera_js = """
290
- async () => {
291
- // 現在のカメラを取得
292
- const video = document.querySelector('video');
293
- if (!video || !video.srcObject) {
294
- alert('カメラを先に起動してください');
295
- return;
296
- }
297
-
298
- const tracks = video.srcObject.getVideoTracks();
299
- if (tracks.length === 0) return;
300
-
301
- const currentSettings = tracks[0].getSettings();
302
- const currentFacing = currentSettings.facingMode || 'user';
303
-
304
- // 反対のカメラに切り替え
305
- const newFacing = currentFacing === 'user' ? 'environment' : 'user';
306
-
307
- try {
308
- // 古いトラックを停止
309
- tracks.forEach(track => track.stop());
310
-
311
- // 新しいカメラを取得
312
- const newStream = await navigator.mediaDevices.getUserMedia({
313
- video: { facingMode: { exact: newFacing } }
314
- });
315
-
316
- video.srcObject = newStream;
317
- console.log('カメラ切り替え成功:', newFacing);
318
- } catch (err) {
319
- console.error('カメラ切り替え失敗:', err);
320
- // exactが失敗した場合、idealで試す
321
- try {
322
- const newStream = await navigator.mediaDevices.getUserMedia({
323
- video: { facingMode: { ideal: newFacing } }
324
- });
325
- video.srcObject = newStream;
326
- } catch (err2) {
327
- alert('カメラの切り替えに失敗しました。このデバイスでは切り替えできない可能性があります。');
328
- }
329
- }
330
- }
331
- """
332
-
333
- # イベントハンドラ
334
- analyze_btn.click(
335
  fn=process_webcam,
336
  inputs=[webcam],
337
- outputs=[webcam, location_output, info_output],
338
- )
339
-
340
- switch_camera_btn.click(
341
- fn=None,
342
- inputs=[],
343
- outputs=[],
344
- js=switch_camera_js,
345
  )
346
 
347
  reset_btn.click(
348
  fn=reset_state,
349
- outputs=[location_output],
350
  )
351
 
352
  gr.Markdown(
353
  """
354
  ---
355
  ### 使い方
356
- 1. カメラを許可して起動(自動で外カメラが選択されます)
357
- 2. 必要に応じて**「内/外カメラ切替」**で切り替え
358
- 3. 周囲の看板や店舗が見える位置で**カメラUI内の撮影ボタン**を押して写真を撮る
359
- 4. **「解析する」**ボタンを押して位置を特定
360
- 5. 複数回解析すると精度が上がります
361
 
362
  ### 注意事項
363
  - GPSは使用していません(映像のみで位置を推定)
364
  - コンビニ、飲食店、駅などが見えると精度が上がります
365
- - **先に写真を撮ってから解析ボタンを押してください**
366
  """
367
  )
368
 
 
6
  """
7
 
8
  import time
 
9
  from typing import Optional
10
  import numpy as np
11
  import gradio as gr
 
46
  self._hint_lon: float = 139.7671
47
 
48
  def process_frame(self, frame: np.ndarray) -> dict:
49
+ """フレームを処理"""
 
 
 
 
 
 
 
 
 
 
50
  if frame is None:
51
  return self._empty_result()
52
 
 
63
 
64
  # OCR処理
65
  if sample.should_ocr:
66
+ print("[DEBUG] Running OCR...")
67
  raw_texts = self.ocr_engine.detect_text_only(frame)
68
  ocr_texts = [clean_ocr_text(t) for t in raw_texts if t]
69
  self._latest_ocr_texts = ocr_texts
70
+ print(f"[DEBUG] OCR detected: {ocr_texts}")
71
 
72
  # VLM処理
73
  if sample.should_vlm and self.vlm_analyzer.is_available:
74
  try:
75
+ print("[DEBUG] Running VLM...")
76
  analysis = self.vlm_analyzer.analyze(frame)
77
  if analysis.success:
78
  self._latest_analysis = analysis
79
  vlm_keywords = self.vlm_analyzer.get_search_keywords(analysis)
80
+ print(f"[DEBUG] VLM keywords: {vlm_keywords}")
81
  except Exception as e:
82
  print(f"VLM error: {e}")
83
 
 
131
  self._latest_ocr_texts = []
132
  self._latest_analysis = None
133
 
 
 
 
 
 
134
 
135
  # グローバルアプリインスタンス
136
  app = DokoCameApp()
137
 
138
 
139
  def process_webcam(frame):
140
+ """Webcam入力を処理"""
 
 
141
  if frame is None:
142
+ return "カメラを起動してください", ""
 
143
 
144
+ print(f"[DEBUG] process_webcam called, frame shape: {frame.shape}")
145
  result = app.process_frame(frame)
 
146
 
147
  # OCRテキストをフォーマット
148
  ocr_display = ""
 
167
  location_display += f"\n\n座標: {r.estimated_lat:.6f}, {r.estimated_lon:.6f}"
168
  location_display += f"\n信頼度: {r.confidence:.1%}"
169
 
170
+ return location_display, info_display
171
 
172
 
173
  def reset_state():
174
  """状態リセット"""
175
  app.reset()
176
+ return "リセットしました", ""
177
 
178
 
179
  def create_ui():
180
  """Gradio UIを作成"""
181
 
182
+ # カスタムCSS - カメラ切り替えボタンをオーバーレイ表示
183
+ custom_css = """
184
+ .camera-container {
185
+ position: relative;
186
+ }
187
+ .camera-switch-btn {
188
+ position: absolute;
189
+ top: 10px;
190
+ right: 10px;
191
+ z-index: 100;
192
+ background: rgba(0,0,0,0.7) !important;
193
+ color: white !important;
194
+ border: none !important;
195
+ padding: 8px 12px !important;
196
+ border-radius: 20px !important;
197
+ font-size: 14px !important;
198
+ }
199
+ .camera-switch-btn:hover {
200
+ background: rgba(0,0,0,0.9) !important;
201
+ }
202
+ """
203
+
204
+ # 初期化JavaScript(外カメラデフォルト + カメラ切り替えボタン追加)
205
+ init_js = """
206
  () => {
207
+ // カメラ切り替えボタンを追加
208
+ const addSwitchButton = () => {
209
+ const imageContainer = document.querySelector('.image-container, [data-testid="image"]');
210
+ if (!imageContainer) {
211
+ setTimeout(addSwitchButton, 500);
212
+ return;
213
+ }
214
+
215
+ // 既にボタンがあれば追加しない
216
+ if (document.querySelector('.camera-switch-overlay')) return;
217
+
218
+ const btn = document.createElement('button');
219
+ btn.className = 'camera-switch-overlay';
220
+ btn.innerHTML = '🔄 内/外';
221
+ btn.style.cssText = 'position:absolute;top:10px;right:10px;z-index:1000;background:rgba(0,0,0,0.7);color:white;border:none;padding:10px 15px;border-radius:25px;font-size:16px;cursor:pointer;';
222
+
223
+ btn.onclick = async () => {
224
+ const video = document.querySelector('video');
225
+ if (!video || !video.srcObject) {
226
+ alert('カメラを先に起動してください');
227
+ return;
228
+ }
229
+
230
+ const tracks = video.srcObject.getVideoTracks();
231
+ if (tracks.length === 0) return;
232
+
233
+ const currentFacing = tracks[0].getSettings().facingMode || 'user';
234
+ const newFacing = currentFacing === 'user' ? 'environment' : 'user';
235
+
236
+ try {
237
+ tracks.forEach(track => track.stop());
238
+ const newStream = await navigator.mediaDevices.getUserMedia({
239
+ video: { facingMode: { ideal: newFacing } }
240
+ });
241
+ video.srcObject = newStream;
242
+ btn.innerHTML = newFacing === 'environment' ? '🔄 外カメラ' : '🔄 内カメラ';
243
+ } catch (err) {
244
+ console.error('カメラ切り替え失敗:', err);
245
+ }
246
+ };
247
+
248
+ imageContainer.style.position = 'relative';
249
+ imageContainer.appendChild(btn);
250
+ };
251
+
252
+ // 外カメラをデフォルトに
253
  const initRearCamera = async () => {
 
254
  await new Promise(r => setTimeout(r, 2000));
255
 
256
  const video = document.querySelector('video');
257
+ if (!video || !video.srcObject) {
 
 
 
 
 
 
 
 
258
  setTimeout(initRearCamera, 1000);
259
  return;
260
  }
261
 
262
  try {
263
  const tracks = video.srcObject.getVideoTracks();
264
+ tracks.forEach(track => track.stop());
 
 
265
 
 
266
  const stream = await navigator.mediaDevices.getUserMedia({
267
  video: { facingMode: { ideal: 'environment' } }
268
  });
 
273
  }
274
  };
275
 
276
+ addSwitchButton();
277
  initRearCamera();
278
+
279
+ // 監視して再追加
280
+ const observer = new MutationObserver(() => {
281
+ if (!document.querySelector('.camera-switch-overlay')) {
282
+ addSwitchButton();
283
+ }
284
+ });
285
+ observer.observe(document.body, { childList: true, subtree: true });
286
  }
287
  """
288
 
289
  with gr.Blocks(
290
  title="どこカメ - リアルタイム位置特定",
291
  theme=gr.themes.Soft(),
292
+ css=custom_css,
293
+ js=init_js,
294
  ) as demo:
295
  gr.Markdown(
296
  """
297
  # 📍 どこカメ (dokoCame)
298
+ **かざすだけで、視界がそのまま住所になる**
299
  """
300
  )
301
 
302
  with gr.Row():
303
  with gr.Column(scale=2):
304
+ # カメラ入力(streaming=Trueで自動解析)
305
  webcam = gr.Image(
306
  sources=["webcam"],
307
+ streaming=True,
308
+ label="カメラ映像",
309
  mirror_webcam=False,
310
+ elem_classes=["camera-container"],
311
  )
312
 
 
 
 
 
 
 
313
  with gr.Column(scale=1):
314
  # 位置情報表示
315
  location_output = gr.Textbox(
 
321
  # 検出情報表示
322
  info_output = gr.Textbox(
323
  label="🔍 検出情報",
324
+ lines=8,
325
  interactive=False,
326
  )
327
 
328
  # リセットボタン
329
  reset_btn = gr.Button("🔄 リセット", variant="secondary")
330
 
331
+ # 自動解析(streaming=Trueで画像が変わるたびに呼ばれる)
332
+ webcam.stream(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  fn=process_webcam,
334
  inputs=[webcam],
335
+ outputs=[location_output, info_output],
 
 
 
 
 
 
 
336
  )
337
 
338
  reset_btn.click(
339
  fn=reset_state,
340
+ outputs=[location_output, info_output],
341
  )
342
 
343
  gr.Markdown(
344
  """
345
  ---
346
  ### 使い方
347
+ 1. カメラを許可(自動で外カメラが選択されます)
348
+ 2. 右上の**「🔄 内/外」**ボタンでカメラ切り替え可能
349
+ 3. 周囲の看板や店舗を映すと**自動で解析**されます
 
 
350
 
351
  ### 注意事項
352
  - GPSは使用していません(映像のみで位置を推定)
353
  - コンビニ、飲食店、駅などが見えると精度が上がります
 
354
  """
355
  )
356
 
core/ocr_engine.py CHANGED
@@ -20,14 +20,12 @@ class OCREngine:
20
  日本語テキスト抽出に最適化
21
  """
22
 
23
- def __init__(self, lang: str = "japan", use_gpu: bool = False):
24
  """
25
  Args:
26
  lang: 言語設定 ("japan", "en", "ch" など)
27
- use_gpu: GPU使用フラグ(Hugging Face Free TierではFalse)
28
  """
29
  self.lang = lang
30
- self.use_gpu = use_gpu
31
  self._ocr = None
32
  self._initialized = False
33
 
@@ -39,16 +37,20 @@ class OCREngine:
39
  try:
40
  from paddleocr import PaddleOCR
41
 
42
- # PaddleOCR 3.x では引数が変更されている
 
43
  self._ocr = PaddleOCR(
44
  use_angle_cls=True,
45
  lang=self.lang,
46
- use_gpu=self.use_gpu,
47
  )
48
  self._initialized = True
 
49
  except ImportError:
50
  print("Warning: PaddleOCR not installed. OCR will not work.")
51
  self._initialized = False
 
 
 
52
 
53
  def detect(self, frame: np.ndarray) -> List[OCRResult]:
54
  """
 
20
  日本語テキスト抽出に最適化
21
  """
22
 
23
+ def __init__(self, lang: str = "japan"):
24
  """
25
  Args:
26
  lang: 言語設定 ("japan", "en", "ch" など)
 
27
  """
28
  self.lang = lang
 
29
  self._ocr = None
30
  self._initialized = False
31
 
 
37
  try:
38
  from paddleocr import PaddleOCR
39
 
40
+ # PaddleOCR 3.x では use_gpu, show_log 等が廃止
41
+ # lang と use_angle_cls のみ使用
42
  self._ocr = PaddleOCR(
43
  use_angle_cls=True,
44
  lang=self.lang,
 
45
  )
46
  self._initialized = True
47
+ print("[OCR] PaddleOCR initialized successfully")
48
  except ImportError:
49
  print("Warning: PaddleOCR not installed. OCR will not work.")
50
  self._initialized = False
51
+ except Exception as e:
52
+ print(f"Warning: PaddleOCR init error: {e}")
53
+ self._initialized = False
54
 
55
  def detect(self, frame: np.ndarray) -> List[OCRResult]:
56
  """