himipo commited on
Commit
488d9e9
·
1 Parent(s): f6a743e

Add PDF upload and YOLO routing for Spaces

Browse files
Files changed (5) hide show
  1. README.md +20 -15
  2. app.py +176 -73
  3. detection.py +264 -18
  4. models/yolobest.pt +3 -0
  5. requirements.txt +7 -4
README.md CHANGED
@@ -1,10 +1,11 @@
1
  ---
2
- title: DEIMv2 Floorplan Symbol Detection
3
  emoji: 🏗️
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: 5.0.0
 
8
  app_file: app.py
9
  pinned: false
10
  license: AItech
@@ -14,36 +15,39 @@ DEMO URL
14
  https://huggingface.co/spaces/himipo/gpu_symbol
15
 
16
 
17
- # DEIMv2 図面記号検出デモ
18
 
19
- DEIMv2 (Detection Enhanced by Interaction Module v2) を使用した図面記号検出デモアプリケーションです。
20
 
21
  ## 機能
22
 
23
- - PNG形式の図面画像からの記号検出(16クラス対応
24
- - タイル推論よる大サイズ画像処理(640×640タイ、128pxオバーラップ)
 
25
  - 検出結果の可視化(バウンディングボックス + ラベル + スコア)
26
  - 記号ごとの個数集計表示
27
  - スコア閾値の調整(デフォルト: 0.9)
28
- - クラスフィルタリング機能(検出するクラスを選択可能)
29
  - NMS(Non-Maximum Suppression)による重複検出の自動マージ
30
 
31
  ## 使用方法
32
 
33
- 1. 左側に **PNG形式の図面** をアップロード
 
34
  2. **詳細設定**を開いて以下を調整(必要に応じて):
35
  - **スコア閾値**: 検出の信頼度閾値(デフォルト: 0.9)
36
- - **検出するクラス**: チェックボックスで検出したいクラスを選択(デフォルト: door1のみ)
37
  3. 「検出を実行」ボタンをクリック
38
  4. 中央に **検出結果付き図面**、右側に **記号名称と個数** が表示されます
39
 
40
  ## モデル
41
 
42
  - **DEIMv2**: DINOv3STAsバックボーン(ViT-Tiny)を使用した物体検出モデル
43
- - **検出対象**: 16クラスの図面記号
 
44
  - `kanki`, `kanki_shikaku`, `kanki_regisuta`
45
- - `window1`, `window2`
46
- - `door1`, `door2`
47
  - `bathtub1`, `konro1`, `sink1`, `toilet1`
48
  - `kasaikeihou1`, `kasaikeihou2`
49
  - `houi1`, `houi2`, `houi3`
@@ -58,9 +62,10 @@ DEIMv2 (Detection Enhanced by Interaction Module v2) を使用した図面記号
58
  ## ファイル構成
59
 
60
  - `app.py`: Gradio UI + 推論パイプライン
61
- - `detection.py`: DEIMv2推論ラッパー(タイル推論、NMS統合)
62
  - `configs/deimv2_floorplan.yaml`: モデル設定ファイル
63
  - `models/best_stg2.pth`: モデル重みファイル(Git LFS)
 
64
  - `engine/`: DEIMv2エンジンモジュール
65
  - `core/`: YAMLConfig関連モジュール
66
  - `yaml_config.py`: YAMLConfigクラス
@@ -77,7 +82,7 @@ DEIMv2 (Detection Enhanced by Interaction Module v2) を使用した図面記号
77
  ## 技術詳細
78
 
79
  ### タイル推論
80
- 大きな画像を640×640ピクセルのタイルに分割して推論します。タイル間は128ピクセルのオーバーラップを持ち、境界付近の記号も確実に検出できます。
81
 
82
  ### NMS(Non-Maximum Suppression)
83
  タイル推論により生じる重複検出を、クラスごとにIoU閾値0.4でNMSを適用して統合します。
@@ -94,6 +99,6 @@ DEIMv2 (Detection Enhanced by Interaction Module v2) を使用した図面記号
94
 
95
  - **モデルファイルが見つからない**: Git LFSが正しく設定されているか確認してください
96
  - **メモリ不足**: CPU UpgradeまたはGPUオプションの使用を検討してください
97
- - **推論エラー**: 画像形式がPNG形式であることを確認してください
98
  - **検出結果が表示されない**: スコア閾値を下げる(例: 0.5)か、検出するクラスをすべて選択してください
99
  - **デバッグモード**: 環境変数 `DEBUG_DEIMV2=1` を設定すると、詳細��デバッグ情報が出力されます
 
1
  ---
2
+ title: PDF/PNG Floorplan Symbol Detection
3
  emoji: 🏗️
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
+ sdk_version: 5.49.1
8
+ python_version: 3.12.12
9
  app_file: app.py
10
  pinned: false
11
  license: AItech
 
15
  https://huggingface.co/spaces/himipo/gpu_symbol
16
 
17
 
18
+ # PDF/PNG 図面記号検出デモ
19
 
20
+ DEIMv2 YOLO26L自動ルーティングして使用する図面記号検出デモアプリケーションです。
21
 
22
  ## 機能
23
 
24
+ - PDF/PNG形式の図面からの記号検出(PDFは1ページ目のみ400dpiで変換
25
+ - 選択クラス応じたDEIMv2 / YOLO26L自動ルーティング
26
+ - タイル推論による大サイズ画像の処理
27
  - 検出結果の可視化(バウンディングボックス + ラベル + スコア)
28
  - 記号ごとの個数集計表示
29
  - スコア閾値の調整(デフォルト: 0.9)
30
+ - クラスフィルタリング機能(対象クラスを選択可能)
31
  - NMS(Non-Maximum Suppression)による重複検出の自動マージ
32
 
33
  ## 使用方法
34
 
35
+ 1. 左側に **PDFまたはPNG形式の図面** をアップロード
36
+ - PDFの場合は1ページ目のみを400dpiで変換して推論します
37
  2. **詳細設定**を開いて以下を調整(必要に応じて):
38
  - **スコア閾値**: 検出の信頼度閾値(デフォルト: 0.9)
39
+ - **対象クラス**: チェックボックスで検出したいクラスを選択(デフォルト: door1のみ)
40
  3. 「検出を実行」ボタンをクリック
41
  4. 中央に **検出結果付き図面**、右側に **記号名称と個数** が表示されます
42
 
43
  ## モデル
44
 
45
  - **DEIMv2**: DINOv3STAsバックボーン(ViT-Tiny)を使用した物体検出モデル
46
+ - **YOLO26L**: 窓・ドア・サッシ系クラスを担当する物体検出モデル
47
+ - **検出対象**: DEIMv2既存クラス + YOLO26L追加クラス
48
  - `kanki`, `kanki_shikaku`, `kanki_regisuta`
49
+ - `window1`, `window2`, `window3`, `window4`, `window_hiki`, `window_hiki2`
50
+ - `door1`, `door2`, `sash_circle`, `sash_rect`
51
  - `bathtub1`, `konro1`, `sink1`, `toilet1`
52
  - `kasaikeihou1`, `kasaikeihou2`
53
  - `houi1`, `houi2`, `houi3`
 
62
  ## ファイル構成
63
 
64
  - `app.py`: Gradio UI + 推論パイプライン
65
+ - `detection.py`: DEIMv2 / YOLO26L推論(タイル推論、NMS統合)
66
  - `configs/deimv2_floorplan.yaml`: モデル設定ファイル
67
  - `models/best_stg2.pth`: モデル重みファイル(Git LFS)
68
+ - `models/yolobest.pt`: YOLO26Lモデル重みファイル(Git LFS)
69
  - `engine/`: DEIMv2エンジンモジュール
70
  - `core/`: YAMLConfig関連モジュール
71
  - `yaml_config.py`: YAMLConfigクラス
 
82
  ## 技術詳細
83
 
84
  ### タイル推論
85
+ 大きな画像をタイルに分割して推論します。DEIMv2は640×640、YOLO26Lは960×960を基本タイルとして使用し、境界付近の記号も検出できるようにオーバーラップを持たせます。
86
 
87
  ### NMS(Non-Maximum Suppression)
88
  タイル推論により生じる重複検出を、クラスごとにIoU閾値0.4でNMSを適用して統合します。
 
99
 
100
  - **モデルファイルが見つからない**: Git LFSが正しく設定されているか確認してください
101
  - **メモリ不足**: CPU UpgradeまたはGPUオプションの使用を検討してください
102
+ - **推論エラー**: 入力PDFまたはPNG形式であることを確認してください
103
  - **検出結果が表示されない**: スコア閾値を下げる(例: 0.5)か、検出するクラスをすべて選択してください
104
  - **デバッグモード**: 環境変数 `DEBUG_DEIMV2=1` を設定すると、詳細��デバッグ情報が出力されます
app.py CHANGED
@@ -1,10 +1,17 @@
1
  # app.py
2
  from collections import Counter
3
  from functools import lru_cache
4
- from typing import Tuple, Dict, Any, List
 
 
5
  import os
 
6
  import yaml
7
 
 
 
 
 
8
  import gradio as gr
9
  import numpy as np
10
  import spaces
@@ -12,31 +19,23 @@ from PIL import Image, ImageDraw, ImageFont
12
 
13
  from detection import run_inference, Detection
14
 
15
- # Gradio 5.0.x と JSON コンポーネントの組み合わせで
16
- # /info 生成時に json_schema_to_python_type が bool を dict とみなして落ちる
17
- # 既知バグがあるため、bool を安全に処理するようにパッチを当てる。
18
- try:
19
- from gradio_client import utils as grc_utils
20
-
21
- _orig_json_schema_to_python_type = grc_utils._json_schema_to_python_type # type: ignore[attr-defined]
22
- _orig_json_schema_to_python_type_public = grc_utils.json_schema_to_python_type
23
-
24
- def _json_schema_to_python_type_safe(schema, defs=None): # type: ignore[override]
25
- if isinstance(schema, bool):
26
- return "Any"
27
- return _orig_json_schema_to_python_type(schema, defs)
28
-
29
- grc_utils._json_schema_to_python_type = _json_schema_to_python_type_safe # type: ignore[attr-defined]
30
-
31
- def _json_schema_to_python_type_safe_public(schema):
32
- if isinstance(schema, bool):
33
- return "Any"
34
- return _orig_json_schema_to_python_type_public(schema)
35
-
36
- grc_utils.json_schema_to_python_type = _json_schema_to_python_type_safe_public
37
- except Exception:
38
- # パッチが失敗してもアプリ起動は継続する
39
- pass
40
 
41
 
42
  @lru_cache(maxsize=1)
@@ -44,30 +43,147 @@ def load_class_names() -> List[str]:
44
  """
45
  設定ファイルからクラスリストを読み込む
46
  """
 
 
 
47
  config_path = "configs/deimv2_floorplan.yaml"
48
  try:
49
  with open(config_path, 'r', encoding='utf-8') as f:
50
  config = yaml.safe_load(f)
51
  # Modelセクションからclass_namesを取得
52
  if 'Model' in config and 'class_names' in config['Model']:
53
- return config['Model']['class_names']
54
  else:
55
  # フォールバック: デフォルトのクラスリスト
56
- return ["kanki", "kanki_shikaku", "kanki_regisuta", "window1", "window2",
57
- "door1", "door2", "bathtub1", "konro1", "sink1", "toilet1",
58
- "kasaikeihou1", "kasaikeihou2", "houi1", "houi2", "houi3"]
59
  except Exception as e:
60
  # エラー時はデフォルトのクラスリストを返す
61
  print(f"Warning: Failed to load class names from config: {e}")
62
- return ["kanki", "kanki_shikaku", "kanki_regisuta", "window1", "window2",
63
- "door1", "door2", "bathtub1", "konro1", "sink1", "toilet1",
64
- "kasaikeihou1", "kasaikeihou2", "houi1", "houi2", "houi3"]
 
 
 
 
65
 
66
 
67
  def pil_to_np(img: Image.Image) -> np.ndarray:
68
  return np.array(img.convert("RGB"))
69
 
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def draw_detections(
72
  image_pil: Image.Image,
73
  detections: List[Detection],
@@ -130,38 +246,21 @@ def summarize_detections(detections: List[Detection]) -> List[List[Any]]:
130
 
131
 
132
  def inference_pipeline(
133
- image: Image.Image,
134
  score_thresh: float = 0.8,
135
  selected_classes: List[str] = None,
136
  ) -> Tuple[Image.Image, List[List[Any]]]:
137
  """Gradio から呼ばれるメイン処理"""
138
- if image is None:
139
- raise gr.Error("PNG形式の図面をアップロードしてください。")
140
-
141
  try:
142
- # 画像が文字列(ファイルパス)の場合はPIL Imageに変換
143
- if isinstance(image, str):
144
- try:
145
- img_pil = Image.open(image).convert("RGB")
146
- except Exception as e:
147
- raise gr.Error(f"画像ファイルの読み込みに失敗しました: {str(e)}")
148
- else:
149
- # 既にPIL Imageオブジェクトの場合
150
- img_pil = image.convert("RGB")
151
-
152
  img_np = pil_to_np(img_pil)
153
 
154
- # DEIMv2 推論
155
- detections = run_inference(img_np, score_thresh=score_thresh)
156
-
157
- # クラスフィルタリング: 選択されたクラスのみを残す
158
- if selected_classes is not None and len(selected_classes) > 0:
159
- # 選択されたクラスリストに含まれる検出結果のみをフィルタリング
160
- filtered_detections = [
161
- det for det in detections
162
- if det[4] in selected_classes # det[4]はlabel_name
163
- ]
164
- detections = filtered_detections
165
 
166
  # 描画
167
  vis_pil = draw_detections(img_pil.copy(), detections)
@@ -170,32 +269,35 @@ def inference_pipeline(
170
  summary = summarize_detections(detections)
171
 
172
  return vis_pil, summary
 
 
173
  except Exception as e:
174
  error_msg = f"エラーが発生しました: {str(e)}"
175
  raise gr.Error(error_msg)
176
 
177
 
178
- @spaces.GPU
179
  def gpu_inference(
180
- image: Image.Image,
181
  score_thresh: float = 0.9, # UIのデフォルト値と統一
182
  selected_classes: List[str] = None,
183
  ):
184
  """Spaces ZeroGPU が検出できるようにデコレータ付きの推論関数を用意"""
185
- return inference_pipeline(image, score_thresh, selected_classes)
186
 
187
 
188
  # =========================
189
  # Gradio UI
190
  # =========================
191
- with gr.Blocks(title="DEIMv2 Floorplan Symbol Detection") as demo:
192
  gr.Markdown(
193
  """
194
  # 図面記号検出デモ(by AItech)
195
 
196
- 1. 左側に **PNG図面** をアップロード
197
- 2. 「検出を実行」を押
198
- 3. 中央に **検出結果付き図面**、右側に **記号名称+個数** が表示されま
 
199
  """
200
  )
201
 
@@ -205,10 +307,10 @@ with gr.Blocks(title="DEIMv2 Floorplan Symbol Detection") as demo:
205
  with gr.Row():
206
  # 左: 入力
207
  with gr.Column(scale=1):
208
- input_image = gr.Image(
209
- label="入力図面 (PNG)",
210
- type="pil",
211
- image_mode="RGB",
212
  )
213
 
214
  # 詳細設定タブ(デフォルトは閉じた状態)
@@ -223,8 +325,8 @@ with gr.Blocks(title="DEIMv2 Floorplan Symbol Detection") as demo:
223
  selected_classes = gr.CheckboxGroup(
224
  choices=class_names,
225
  value=["door1"], # デフォルトでdoor1のみ選択
226
- label="検出するクラス",
227
- info="選択したクラスの検出結果のみが表示されます",
228
  )
229
 
230
  run_button = gr.Button("検出を実行", variant="primary")
@@ -247,14 +349,15 @@ with gr.Blocks(title="DEIMv2 Floorplan Symbol Detection") as demo:
247
  # ボタンの動作
248
  run_button.click(
249
  fn=gpu_inference,
250
- inputs=[input_image, score_thresh, selected_classes],
251
  outputs=[output_image, summary_dataframe],
252
  )
253
 
254
  # Gradio 5では、Spaces上ではdemoオブジェクトを直接エクスポートするだけで動作します
255
  # ローカルテスト時のみdemo.launch()を呼び出します
256
  if __name__ == "__main__":
257
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
258
 
259
  # Spaces上では、demoオブジェクトを直接エクスポートします
260
  # Gradio 5は自動的にdemoオブジェクトを検出して起動します
 
1
  # app.py
2
  from collections import Counter
3
  from functools import lru_cache
4
+ import math
5
+ from pathlib import Path
6
+ from typing import Tuple, Any, List
7
  import os
8
+ import warnings
9
  import yaml
10
 
11
+ os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
12
+ os.environ.setdefault("YOLO_CONFIG_DIR", "/tmp/ultralytics")
13
+ os.makedirs(os.environ["YOLO_CONFIG_DIR"], exist_ok=True)
14
+
15
  import gradio as gr
16
  import numpy as np
17
  import spaces
 
19
 
20
  from detection import run_inference, Detection
21
 
22
+ PDF_DPI = 400
23
+ MAX_RENDERED_PIXELS = int(os.getenv("MAX_RENDERED_PIXELS", "120000000"))
24
+ SUPPORTED_IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp", ".webp"}
25
+ Image.MAX_IMAGE_PIXELS = MAX_RENDERED_PIXELS
26
+ warnings.simplefilter("error", Image.DecompressionBombWarning)
27
+ YOLO_CLASS_NAMES = [
28
+ "window1",
29
+ "window2",
30
+ "window3",
31
+ "window4",
32
+ "window_hiki",
33
+ "window_hiki2",
34
+ "door1",
35
+ "door2",
36
+ "sash_circle",
37
+ "sash_rect",
38
+ ]
 
 
 
 
 
 
 
 
39
 
40
 
41
  @lru_cache(maxsize=1)
 
43
  """
44
  設定ファイルからクラスリストを読み込む
45
  """
46
+ default_class_names = ["kanki", "kanki_shikaku", "kanki_regisuta", "window1", "window2",
47
+ "door1", "door2", "bathtub1", "konro1", "sink1", "toilet1",
48
+ "kasaikeihou1", "kasaikeihou2", "houi1", "houi2", "houi3"]
49
  config_path = "configs/deimv2_floorplan.yaml"
50
  try:
51
  with open(config_path, 'r', encoding='utf-8') as f:
52
  config = yaml.safe_load(f)
53
  # Modelセクションからclass_namesを取得
54
  if 'Model' in config and 'class_names' in config['Model']:
55
+ class_names = config['Model']['class_names']
56
  else:
57
  # フォールバック: デフォルトのクラスリスト
58
+ class_names = default_class_names
 
 
59
  except Exception as e:
60
  # エラー時はデフォルトのクラスリストを返す
61
  print(f"Warning: Failed to load class names from config: {e}")
62
+ class_names = default_class_names
63
+
64
+ merged = list(class_names)
65
+ for class_name in YOLO_CLASS_NAMES:
66
+ if class_name not in merged:
67
+ merged.append(class_name)
68
+ return merged
69
 
70
 
71
  def pil_to_np(img: Image.Image) -> np.ndarray:
72
  return np.array(img.convert("RGB"))
73
 
74
 
75
+ def detect_upload_kind(path: Path) -> str:
76
+ """マジックバイト優先でアップロード種別を判定する。"""
77
+ suffix = path.suffix.lower()
78
+
79
+ try:
80
+ with open(path, "rb") as f:
81
+ header = f.read(16)
82
+ except Exception:
83
+ return "unknown"
84
+
85
+ if header.startswith(b"%PDF"):
86
+ return "pdf"
87
+ if header.startswith(b"\x89PNG\r\n\x1a\n"):
88
+ return "image"
89
+ if header.startswith(b"\xff\xd8\xff"):
90
+ return "image"
91
+ if header[:4] in {b"II*\x00", b"MM\x00*"}:
92
+ return "image"
93
+ if header.startswith(b"RIFF") and header[8:12] == b"WEBP":
94
+ return "image"
95
+ if suffix == ".pdf":
96
+ return "pdf"
97
+ if suffix in SUPPORTED_IMAGE_SUFFIXES:
98
+ return "image"
99
+ return "unknown"
100
+
101
+
102
+ def ensure_pixel_limit(width: int, height: int, source_label: str) -> None:
103
+ pixels = int(width) * int(height)
104
+ if pixels > MAX_RENDERED_PIXELS:
105
+ raise gr.Error(
106
+ f"{source_label}が大きすぎます。"
107
+ f"サイズ: {width}x{height}px / 上限: {MAX_RENDERED_PIXELS:,}px"
108
+ )
109
+
110
+
111
+ def extract_upload_path(upload: Any) -> Path:
112
+ if isinstance(upload, dict):
113
+ upload_path = upload.get("path")
114
+ elif isinstance(upload, (str, os.PathLike)):
115
+ upload_path = upload
116
+ else:
117
+ upload_path = getattr(upload, "path", None)
118
+ if upload_path is None:
119
+ candidate = getattr(upload, "name", None)
120
+ if isinstance(candidate, (str, os.PathLike)) and Path(candidate).exists():
121
+ upload_path = candidate
122
+
123
+ if not isinstance(upload_path, (str, os.PathLike)):
124
+ raise gr.Error("アップロードファイルを読み込めませんでした。")
125
+
126
+ path = Path(upload_path)
127
+ if not path.exists():
128
+ raise gr.Error("アップロードファイルが見つかりません。")
129
+ return path
130
+
131
+
132
+ def normalize_upload_to_pil(upload: Any) -> Image.Image:
133
+ """PDF/画像アップロードを推論用のRGB PIL画像へ正規化する。"""
134
+ if upload is None:
135
+ raise gr.Error("PDFまたはPNG形式の図面をアップロードしてください。")
136
+
137
+ if isinstance(upload, Image.Image):
138
+ ensure_pixel_limit(upload.width, upload.height, "画像")
139
+ return upload.convert("RGB")
140
+
141
+ path = extract_upload_path(upload)
142
+ upload_kind = detect_upload_kind(path)
143
+
144
+ if upload_kind == "pdf":
145
+ try:
146
+ import fitz
147
+
148
+ with fitz.open(str(path)) as doc:
149
+ if doc.needs_pass or doc.is_encrypted:
150
+ raise gr.Error("暗号化またはパスワード保護されたPDFには対応していません。")
151
+ if doc.page_count < 1:
152
+ raise gr.Error("PDFにページがありません。")
153
+ page = doc.load_page(0)
154
+ scale = PDF_DPI / 72.0
155
+ expected_width = math.ceil(page.rect.width * scale)
156
+ expected_height = math.ceil(page.rect.height * scale)
157
+ ensure_pixel_limit(expected_width, expected_height, "PDFの400dpi変換結果")
158
+ pix = page.get_pixmap(dpi=PDF_DPI, colorspace=fitz.csRGB, alpha=False)
159
+ ensure_pixel_limit(pix.width, pix.height, "PDFの400dpi変換結果")
160
+ if pix.n != 3:
161
+ raise gr.Error("PDFのRGB変換に失敗しました。")
162
+ image = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
163
+ return image.convert("RGB")
164
+ except gr.Error:
165
+ raise
166
+ except Exception as e:
167
+ raise gr.Error(f"PDFの読み込みに失敗しました: {str(e)}")
168
+
169
+ if upload_kind == "image":
170
+ try:
171
+ with Image.open(path) as img:
172
+ ensure_pixel_limit(img.width, img.height, "画像")
173
+ img.verify()
174
+ with Image.open(path) as img:
175
+ ensure_pixel_limit(img.width, img.height, "画像")
176
+ return img.convert("RGB")
177
+ except Image.DecompressionBombWarning:
178
+ raise gr.Error("画像が大きすぎるため処理できません。")
179
+ except Image.DecompressionBombError:
180
+ raise gr.Error("画像が大きすぎるため処理できません。")
181
+ except Exception as e:
182
+ raise gr.Error(f"画像ファイルの読み込みに失敗しました: {str(e)}")
183
+
184
+ raise gr.Error("対応形式はPDFまたはPNG画像です。")
185
+
186
+
187
  def draw_detections(
188
  image_pil: Image.Image,
189
  detections: List[Detection],
 
246
 
247
 
248
  def inference_pipeline(
249
+ upload: Any,
250
  score_thresh: float = 0.8,
251
  selected_classes: List[str] = None,
252
  ) -> Tuple[Image.Image, List[List[Any]]]:
253
  """Gradio から呼ばれるメイン処理"""
 
 
 
254
  try:
255
+ img_pil = normalize_upload_to_pil(upload)
 
 
 
 
 
 
 
 
 
256
  img_np = pil_to_np(img_pil)
257
 
258
+ # 選択クラスに応じて detection.py 側で YOLO26L / DEIMv2 を自動ルーティング
259
+ detections = run_inference(
260
+ img_np,
261
+ score_thresh=score_thresh,
262
+ selected_classes=selected_classes,
263
+ )
 
 
 
 
 
264
 
265
  # 描画
266
  vis_pil = draw_detections(img_pil.copy(), detections)
 
269
  summary = summarize_detections(detections)
270
 
271
  return vis_pil, summary
272
+ except gr.Error:
273
+ raise
274
  except Exception as e:
275
  error_msg = f"エラーが発生しました: {str(e)}"
276
  raise gr.Error(error_msg)
277
 
278
 
279
+ @spaces.GPU(duration=180)
280
  def gpu_inference(
281
+ upload: Any,
282
  score_thresh: float = 0.9, # UIのデフォルト値と統一
283
  selected_classes: List[str] = None,
284
  ):
285
  """Spaces ZeroGPU が検出できるようにデコレータ付きの推論関数を用意"""
286
+ return inference_pipeline(upload, score_thresh, selected_classes)
287
 
288
 
289
  # =========================
290
  # Gradio UI
291
  # =========================
292
+ with gr.Blocks(title="PDF/PNG Floorplan Symbol Detection") as demo:
293
  gr.Markdown(
294
  """
295
  # 図面記号検出デモ(by AItech)
296
 
297
+ 1. 左側に **PDFまたはPNG図面** をアップロード
298
+ 2. PDFの場合は **1ページ目のみ400dpiで変換** して推論しま
299
+ 3. 対象クラスを選択して「検出を実行」を押
300
+ 4. 中央に **検出結果付き図面**、右側に **記号名称+個数** が表示されます。
301
  """
302
  )
303
 
 
307
  with gr.Row():
308
  # 左: 入力
309
  with gr.Column(scale=1):
310
+ input_file = gr.File(
311
+ label="PDF/PNG図面アップロード",
312
+ file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp", ".webp"],
313
+ type="filepath",
314
  )
315
 
316
  # 詳細設定タブ(デフォルトは閉じた状態)
 
325
  selected_classes = gr.CheckboxGroup(
326
  choices=class_names,
327
  value=["door1"], # デフォルトでdoor1のみ選択
328
+ label="対象クラス",
329
+ info="選択したクラスに応じてYOLO26LたはDEIMv2で自動推論します",
330
  )
331
 
332
  run_button = gr.Button("検出を実行", variant="primary")
 
349
  # ボタンの動作
350
  run_button.click(
351
  fn=gpu_inference,
352
+ inputs=[input_file, score_thresh, selected_classes],
353
  outputs=[output_image, summary_dataframe],
354
  )
355
 
356
  # Gradio 5では、Spaces上ではdemoオブジェクトを直接エクスポートするだけで動作します
357
  # ローカルテスト時のみdemo.launch()を呼び出します
358
  if __name__ == "__main__":
359
+ server_port = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
360
+ demo.launch(server_name="0.0.0.0", server_port=server_port)
361
 
362
  # Spaces上では、demoオブジェクトを直接エクスポートします
363
  # Gradio 5は自動的にdemoオブジェクトを検出して起動します
detection.py CHANGED
@@ -1,7 +1,9 @@
1
  # detection.py
 
2
  from functools import lru_cache
3
- from typing import List, Tuple
4
  import os
 
5
  import numpy as np
6
  import torch
7
  import torch.nn as nn
@@ -10,6 +12,9 @@ import torchvision.transforms as T
10
 
11
  # デバッグ出力制御フラグ(環境変数で制御)
12
  DEBUG_DEIMV2 = os.getenv("DEBUG_DEIMV2", "0") == "1"
 
 
 
13
 
14
  # YAMLConfigをインポート(engineパッケージ経由でレジストリをロード)
15
  # モジュール登録のために、すべての必要なモジュールを明示的にインポート
@@ -50,6 +55,26 @@ Detection = Tuple[float, float, float, float, str, float]
50
  # ★ここを自分のファイル名に合わせる
51
  MODEL_CONFIG_PATH = "configs/deimv2_floorplan.yaml"
52
  MODEL_WEIGHTS_PATH = "models/best_stg2.pth"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
 
55
  def _get_device():
@@ -60,13 +85,28 @@ def _get_device():
60
  """
61
  return torch.device("cuda" if torch.cuda.is_available() else "cpu")
62
 
63
- # クラスID→記号名マッピング
64
- # クラス名リスト: ["kanki", "kanki_shikaku", "kanki_regisuta", "window1", "window2", "door1", "door2", "bathtub1", "konro1", "sink1", "toilet1", "kasaikeihou1", "kasaikeihou2", "houi1", "houi2", "houi3"]
65
- label_map = {
66
- 0: "kanki", # kanki
67
- 5: "door1",
68
- 6: "door2",
69
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
 
72
  @lru_cache(maxsize=1)
@@ -427,7 +467,7 @@ def run_inference_single_tile(
427
  unique_labels, label_counts = np.unique(labels, return_counts=True)
428
  print(f"[DEBUG] ラベル分布:")
429
  for label_id, count in zip(unique_labels, label_counts):
430
- label_name = label_map.get(int(label_id), f"class_{int(label_id)}")
431
  print(f" - クラスID {int(label_id)} ({label_name}): {count}件")
432
 
433
  # スコア閾値以上の検出数
@@ -440,7 +480,7 @@ def run_inference_single_tile(
440
  print(f"[DEBUG] 上位5件の検出結果:")
441
  for rank, idx in enumerate(top_indices, 1):
442
  label_id = int(labels[idx])
443
- label_name = label_map.get(label_id, f"class_{label_id}")
444
  score = float(scores[idx])
445
  x1, y1, x2, y2 = boxes[idx]
446
  print(f" [{rank}] {label_name}, スコア={score:.4f}, bbox=({x1:.1f},{y1:.1f},{x2:.1f},{y2:.1f})")
@@ -457,10 +497,10 @@ def run_inference_single_tile(
457
 
458
  x1, y1, x2, y2 = [float(v) for v in box.tolist()]
459
  label_id = int(label)
460
- label_name = label_map.get(label_id, f"class_{label_id}")
461
 
462
- # label_mapに存在しないクラスもカウント(デバッグ用)
463
- if label_id not in label_map:
464
  filtered_by_label += 1
465
 
466
  # タイル座標を元の画像座標に変換
@@ -472,16 +512,17 @@ def run_inference_single_tile(
472
  detections.append((x1_orig, y1_orig, x2_orig, y2_orig, label_name, score))
473
 
474
  if DEBUG_DEIMV2 and (filtered_by_thresh > 0 or filtered_by_label > 0):
475
- print(f"[DEBUG] フィ���タリング: スコア閾値で{filtered_by_thresh}件、label_mapで{filtered_by_label}件除外")
476
 
477
  return detections
478
 
479
 
480
- def run_inference(
481
  image_np: np.ndarray,
482
  score_thresh: float = 0.8,
483
- tile_size: int = 640,
484
- tile_overlap: int = 128,
 
485
  ) -> List[Detection]:
486
  """
487
  タイル推論を実行する。
@@ -490,6 +531,7 @@ def run_inference(
490
  Args:
491
  image_np: RGB np.ndarray (H, W, 3)
492
  score_thresh: スコア閾値
 
493
  tile_size: タイルサイズ(デフォルト: 640)
494
  tile_overlap: タイル間のオーバーラップ(デフォルト: 128)
495
 
@@ -511,6 +553,7 @@ def run_inference(
511
 
512
  im_pil = Image.fromarray(image_np).convert("RGB")
513
  img_w, img_h = im_pil.size
 
514
 
515
  if DEBUG_DEIMV2:
516
  print(f"[DEBUG] ===== タイル推論開始 =====")
@@ -574,7 +617,10 @@ def run_inference(
574
 
575
  if DEBUG_DEIMV2:
576
  print(f"[DEBUG] 検出数: {len(tile_detections)}件")
577
- all_detections.extend(tile_detections)
 
 
 
578
 
579
  if DEBUG_DEIMV2:
580
  print(f"[DEBUG] 総検出数(重複あり): {len(all_detections)}件")
@@ -670,3 +716,203 @@ def run_inference(
670
  return merged_detections
671
  except Exception as e:
672
  raise RuntimeError(f"推論の実行に失敗しました: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # detection.py
2
+ from collections import Counter
3
  from functools import lru_cache
4
+ from typing import Dict, List, Optional, Set, Tuple
5
  import os
6
+ import yaml
7
  import numpy as np
8
  import torch
9
  import torch.nn as nn
 
12
 
13
  # デバッグ出力制御フラグ(環境変数で制御)
14
  DEBUG_DEIMV2 = os.getenv("DEBUG_DEIMV2", "0") == "1"
15
+ YOLO_CONFIG_DIR = os.getenv("YOLO_CONFIG_DIR", "/tmp/ultralytics")
16
+ os.makedirs(YOLO_CONFIG_DIR, exist_ok=True)
17
+ os.environ.setdefault("YOLO_CONFIG_DIR", YOLO_CONFIG_DIR)
18
 
19
  # YAMLConfigをインポート(engineパッケージ経由でレジストリをロード)
20
  # モジュール登録のために、すべての必要なモジュールを明示的にインポート
 
55
  # ★ここを自分のファイル名に合わせる
56
  MODEL_CONFIG_PATH = "configs/deimv2_floorplan.yaml"
57
  MODEL_WEIGHTS_PATH = "models/best_stg2.pth"
58
+ YOLO_WEIGHTS_PATH = "models/yolobest.pt"
59
+ YOLO_TILE_SIZE = 960
60
+ YOLO_TILE_OVERLAP = 192
61
+ DEIM_TILE_SIZE = 640
62
+ DEIM_TILE_OVERLAP = 128
63
+ TILE_MERGE_IOU_THRESHOLD = 0.4
64
+
65
+ YOLO_CLASS_NAMES = [
66
+ "window1",
67
+ "window2",
68
+ "window3",
69
+ "window4",
70
+ "window_hiki",
71
+ "window_hiki2",
72
+ "door1",
73
+ "door2",
74
+ "sash_circle",
75
+ "sash_rect",
76
+ ]
77
+ YOLO_CLASSES: Set[str] = set(YOLO_CLASS_NAMES)
78
 
79
 
80
  def _get_device():
 
85
  """
86
  return torch.device("cuda" if torch.cuda.is_available() else "cpu")
87
 
88
+
89
+ def _get_device_key() -> str:
90
+ return "cuda:0" if torch.cuda.is_available() else "cpu"
91
+
92
+ def load_deimv2_class_names() -> List[str]:
93
+ default_class_names = ["kanki", "kanki_shikaku", "kanki_regisuta", "window1", "window2",
94
+ "door1", "door2", "bathtub1", "konro1", "sink1", "toilet1",
95
+ "kasaikeihou1", "kasaikeihou2", "houi1", "houi2", "houi3"]
96
+ try:
97
+ with open(MODEL_CONFIG_PATH, "r", encoding="utf-8") as f:
98
+ config = yaml.safe_load(f)
99
+ class_names = config.get("Model", {}).get("class_names")
100
+ if isinstance(class_names, list) and class_names:
101
+ return [str(name) for name in class_names]
102
+ except Exception as e:
103
+ print(f"Warning: Failed to load DEIMv2 class names: {e}")
104
+ return default_class_names
105
+
106
+
107
+ DEIM_CLASS_NAMES = load_deimv2_class_names()
108
+ DEIM_LABEL_MAP: Dict[int, str] = {idx: name for idx, name in enumerate(DEIM_CLASS_NAMES)}
109
+ DEIM_CLASSES: Set[str] = set(DEIM_CLASS_NAMES) - YOLO_CLASSES
110
 
111
 
112
  @lru_cache(maxsize=1)
 
467
  unique_labels, label_counts = np.unique(labels, return_counts=True)
468
  print(f"[DEBUG] ラベル分布:")
469
  for label_id, count in zip(unique_labels, label_counts):
470
+ label_name = DEIM_LABEL_MAP.get(int(label_id), f"class_{int(label_id)}")
471
  print(f" - クラスID {int(label_id)} ({label_name}): {count}件")
472
 
473
  # スコア閾値以上の検出数
 
480
  print(f"[DEBUG] 上位5件の検出結果:")
481
  for rank, idx in enumerate(top_indices, 1):
482
  label_id = int(labels[idx])
483
+ label_name = DEIM_LABEL_MAP.get(label_id, f"class_{label_id}")
484
  score = float(scores[idx])
485
  x1, y1, x2, y2 = boxes[idx]
486
  print(f" [{rank}] {label_name}, スコア={score:.4f}, bbox=({x1:.1f},{y1:.1f},{x2:.1f},{y2:.1f})")
 
497
 
498
  x1, y1, x2, y2 = [float(v) for v in box.tolist()]
499
  label_id = int(label)
500
+ label_name = DEIM_LABEL_MAP.get(label_id, f"class_{label_id}")
501
 
502
+ # DEIM_LABEL_MAPに存在しないクラスもカウント(デバッグ用)
503
+ if label_id not in DEIM_LABEL_MAP:
504
  filtered_by_label += 1
505
 
506
  # タイル座標を元の画像座標に変換
 
512
  detections.append((x1_orig, y1_orig, x2_orig, y2_orig, label_name, score))
513
 
514
  if DEBUG_DEIMV2 and (filtered_by_thresh > 0 or filtered_by_label > 0):
515
+ print(f"[DEBUG] フィタリング: スコア閾値で{filtered_by_thresh}件、DEIM_LABEL_MAPで{filtered_by_label}件除外")
516
 
517
  return detections
518
 
519
 
520
+ def run_deimv2_inference(
521
  image_np: np.ndarray,
522
  score_thresh: float = 0.8,
523
+ selected_classes: Optional[Set[str]] = None,
524
+ tile_size: int = DEIM_TILE_SIZE,
525
+ tile_overlap: int = DEIM_TILE_OVERLAP,
526
  ) -> List[Detection]:
527
  """
528
  タイル推論を実行する。
 
531
  Args:
532
  image_np: RGB np.ndarray (H, W, 3)
533
  score_thresh: スコア閾値
534
+ selected_classes: DEIMv2で返すクラス。Noneまたは空の場合はDEIM担当クラスすべて
535
  tile_size: タイルサイズ(デフォルト: 640)
536
  tile_overlap: タイル間のオーバーラップ(デフォルト: 128)
537
 
 
553
 
554
  im_pil = Image.fromarray(image_np).convert("RGB")
555
  img_w, img_h = im_pil.size
556
+ allowed_classes = set(selected_classes) if selected_classes else set(DEIM_CLASSES)
557
 
558
  if DEBUG_DEIMV2:
559
  print(f"[DEBUG] ===== タイル推論開始 =====")
 
617
 
618
  if DEBUG_DEIMV2:
619
  print(f"[DEBUG] 検出数: {len(tile_detections)}件")
620
+ all_detections.extend(
621
+ det for det in tile_detections
622
+ if det[4] in allowed_classes
623
+ )
624
 
625
  if DEBUG_DEIMV2:
626
  print(f"[DEBUG] 総検出数(重複あり): {len(all_detections)}件")
 
716
  return merged_detections
717
  except Exception as e:
718
  raise RuntimeError(f"推論の実行に失敗しました: {e}")
719
+
720
+
721
+ @lru_cache(maxsize=2)
722
+ def load_yolo_model(device_key: str):
723
+ """YOLO26Lモデルをデバイス単位で遅延ロードする。"""
724
+ weights_path = os.path.abspath(YOLO_WEIGHTS_PATH)
725
+ if not os.path.exists(weights_path):
726
+ raise FileNotFoundError(f"YOLOモデルファイルが見つかりません: {weights_path}")
727
+
728
+ try:
729
+ from ultralytics import YOLO
730
+
731
+ model = YOLO(weights_path)
732
+ names = getattr(model.model, "names", None) or getattr(model, "names", None)
733
+ class_map = normalize_yolo_class_map(names)
734
+ expected_map = {idx: name for idx, name in enumerate(YOLO_CLASS_NAMES)}
735
+ if class_map != expected_map:
736
+ raise RuntimeError(
737
+ f"YOLOクラス名が想定と異なります: {class_map}. "
738
+ f"expected={expected_map}"
739
+ )
740
+ return model, class_map
741
+ except Exception as e:
742
+ raise RuntimeError(f"YOLOモデルの読み込みに失敗しました: {e}")
743
+
744
+
745
+ def normalize_yolo_class_map(names) -> Dict[int, str]:
746
+ if isinstance(names, dict):
747
+ return {int(idx): str(name) for idx, name in names.items()}
748
+ if isinstance(names, (list, tuple)):
749
+ return {idx: str(name) for idx, name in enumerate(names)}
750
+ raise RuntimeError(f"YOLOクラス名を読み取れません: {names}")
751
+
752
+
753
+ def clamp_detection(det: Detection, img_w: int, img_h: int) -> Optional[Detection]:
754
+ x1, y1, x2, y2, label_name, score = det
755
+ x1 = max(0.0, min(float(x1), float(img_w)))
756
+ y1 = max(0.0, min(float(y1), float(img_h)))
757
+ x2 = max(0.0, min(float(x2), float(img_w)))
758
+ y2 = max(0.0, min(float(y2), float(img_h)))
759
+ if x2 <= x1 or y2 <= y1:
760
+ return None
761
+ return (x1, y1, x2, y2, label_name, float(score))
762
+
763
+
764
+ def merge_detections_by_class(
765
+ detections: List[Detection],
766
+ device: Optional[torch.device] = None,
767
+ iou_threshold: float = TILE_MERGE_IOU_THRESHOLD,
768
+ ) -> List[Detection]:
769
+ if not detections:
770
+ return []
771
+
772
+ from torchvision.ops import nms
773
+
774
+ target_device = device or torch.device("cpu")
775
+ detections_by_class: Dict[str, List[Tuple[float, float, float, float, float]]] = {}
776
+ for x1, y1, x2, y2, label_name, score in detections:
777
+ detections_by_class.setdefault(label_name, []).append((x1, y1, x2, y2, score))
778
+
779
+ merged: List[Detection] = []
780
+ for label_name, boxes_scores in detections_by_class.items():
781
+ boxes_tensor = torch.tensor(
782
+ [[x1, y1, x2, y2] for x1, y1, x2, y2, _ in boxes_scores],
783
+ device=target_device,
784
+ dtype=torch.float32,
785
+ )
786
+ scores_tensor = torch.tensor(
787
+ [score for _, _, _, _, score in boxes_scores],
788
+ device=target_device,
789
+ dtype=torch.float32,
790
+ )
791
+ keep_indices = nms(boxes_tensor, scores_tensor, iou_threshold=iou_threshold)
792
+ for idx in keep_indices.cpu().numpy():
793
+ x1, y1, x2, y2, score = boxes_scores[int(idx)]
794
+ merged.append((x1, y1, x2, y2, label_name, score))
795
+ return merged
796
+
797
+
798
+ def run_yolo_inference(
799
+ image_np: np.ndarray,
800
+ score_thresh: float = 0.8,
801
+ selected_classes: Optional[Set[str]] = None,
802
+ tile_size: int = YOLO_TILE_SIZE,
803
+ tile_overlap: int = YOLO_TILE_OVERLAP,
804
+ ) -> List[Detection]:
805
+ """YOLO26Lタイル推論を実行し、既存Detection形式で返す。"""
806
+ if image_np.dtype != np.uint8:
807
+ if image_np.max() <= 1.0:
808
+ image_np = (image_np * 255).astype(np.uint8)
809
+ else:
810
+ image_np = image_np.astype(np.uint8)
811
+
812
+ im_pil = Image.fromarray(image_np).convert("RGB")
813
+ img_w, img_h = im_pil.size
814
+ allowed_classes = set(selected_classes) if selected_classes else set(YOLO_CLASSES)
815
+ if not allowed_classes:
816
+ return []
817
+
818
+ device = _get_device()
819
+ device_key = _get_device_key()
820
+ model, class_map = load_yolo_model(device_key)
821
+ selected_class_ids = sorted(
822
+ class_id for class_id, label_name in class_map.items()
823
+ if label_name in allowed_classes
824
+ )
825
+ if not selected_class_ids:
826
+ return []
827
+
828
+ step = tile_size - tile_overlap
829
+ all_detections: List[Detection] = []
830
+
831
+ for tile_y in range(0, img_h, step):
832
+ for tile_x in range(0, img_w, step):
833
+ tile_x_end = min(tile_x + tile_size, img_w)
834
+ tile_y_end = min(tile_y + tile_size, img_h)
835
+ tile = im_pil.crop((tile_x, tile_y, tile_x_end, tile_y_end))
836
+
837
+ with torch.inference_mode():
838
+ results = model.predict(
839
+ tile,
840
+ imgsz=tile_size,
841
+ conf=score_thresh,
842
+ iou=TILE_MERGE_IOU_THRESHOLD,
843
+ classes=selected_class_ids,
844
+ verbose=False,
845
+ device=device_key,
846
+ )
847
+
848
+ if not results:
849
+ continue
850
+ boxes = getattr(results[0], "boxes", None)
851
+ if boxes is None or len(boxes) == 0:
852
+ continue
853
+
854
+ xyxy = boxes.xyxy.detach().cpu().numpy()
855
+ confs = boxes.conf.detach().cpu().numpy()
856
+ classes = boxes.cls.detach().cpu().numpy().astype(int)
857
+ for box, conf, class_id in zip(xyxy, confs, classes):
858
+ label_name = class_map.get(int(class_id), f"class_{int(class_id)}")
859
+ if label_name not in allowed_classes:
860
+ continue
861
+ x1, y1, x2, y2 = [float(v) for v in box.tolist()]
862
+ det = clamp_detection(
863
+ (x1 + tile_x, y1 + tile_y, x2 + tile_x, y2 + tile_y, label_name, float(conf)),
864
+ img_w,
865
+ img_h,
866
+ )
867
+ if det is not None:
868
+ all_detections.append(det)
869
+
870
+ return merge_detections_by_class(all_detections, device=device)
871
+
872
+
873
+ def resolve_requested_classes(selected_classes: Optional[List[str]]) -> Tuple[Set[str], Set[str]]:
874
+ requested = set(selected_classes or [])
875
+ if not requested:
876
+ return set(DEIM_CLASSES), set(YOLO_CLASSES)
877
+
878
+ deim_requested = requested & DEIM_CLASSES
879
+ yolo_requested = requested & YOLO_CLASSES
880
+ unknown_requested = requested - DEIM_CLASSES - YOLO_CLASSES
881
+ if unknown_requested:
882
+ print(f"Warning: Unknown selected classes ignored: {sorted(unknown_requested)}")
883
+ return deim_requested, yolo_requested
884
+
885
+
886
+ def run_inference(
887
+ image_np: np.ndarray,
888
+ score_thresh: float = 0.8,
889
+ selected_classes: Optional[List[str]] = None,
890
+ ) -> List[Detection]:
891
+ """
892
+ 選択クラスに応じてDEIMv2/YOLO26Lを自動ルーティングして推論する。
893
+ """
894
+ try:
895
+ deim_classes, yolo_classes = resolve_requested_classes(selected_classes)
896
+ all_detections: List[Detection] = []
897
+
898
+ if deim_classes:
899
+ all_detections.extend(
900
+ run_deimv2_inference(
901
+ image_np,
902
+ score_thresh=score_thresh,
903
+ selected_classes=deim_classes,
904
+ )
905
+ )
906
+
907
+ if yolo_classes:
908
+ all_detections.extend(
909
+ run_yolo_inference(
910
+ image_np,
911
+ score_thresh=score_thresh,
912
+ selected_classes=yolo_classes,
913
+ )
914
+ )
915
+
916
+ return all_detections
917
+ except Exception as e:
918
+ raise RuntimeError(f"推論の実行に失敗しました: {e}")
models/yolobest.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1e80c2026193a5cd7d6ea341d958dc43a70a40ee0c51028b9a6959a48a5a1d5e
3
+ size 52647821
requirements.txt CHANGED
@@ -1,11 +1,12 @@
1
- gradio>=5.1.0
 
2
  spaces
3
- torch==2.5.1
4
- torchvision==0.20.1
5
  numpy
6
  Pillow>=9.0.0
7
  opencv-python-headless
8
- huggingface_hub==0.25.2
9
  PyYAML>=6.0
10
  tensorboard
11
  scipy>=1.7.0
@@ -13,6 +14,8 @@ faster-coco-eval>=1.6.7
13
  calflops
14
  transformers
15
  onnxruntime>=1.16.0
 
 
16
  # DEIMv2関連の依存関係
17
  timm
18
  omegaconf
 
1
+ gradio==5.49.1
2
+ python-multipart>=0.0.20
3
  spaces
4
+ torch==2.8.0
5
+ torchvision==0.23.0
6
  numpy
7
  Pillow>=9.0.0
8
  opencv-python-headless
9
+ huggingface_hub>=0.33.5
10
  PyYAML>=6.0
11
  tensorboard
12
  scipy>=1.7.0
 
14
  calflops
15
  transformers
16
  onnxruntime>=1.16.0
17
+ PyMuPDF>=1.24.0
18
+ ultralytics>=8.3.0
19
  # DEIMv2関連の依存関係
20
  timm
21
  omegaconf