Spaces:
Sleeping
Sleeping
Mirror current local backend deployment
Browse files- ai_providers.py +0 -262
- benchmark/__init__.py +0 -0
- benchmark/ground_truth.example.json +0 -32
- benchmark/ground_truth.json +0 -25
- benchmark/metrics.py +0 -122
- benchmark/results/after_fix.json +0 -160
- benchmark/results/benchmark_20260629_114932.json +0 -61
- benchmark/results/benchmark_20260629_115100.json +0 -92
- benchmark/results/benchmark_20260707_110502.json +0 -139
- benchmark/results/benchmark_20260707_111530.json +0 -121
- benchmark/results/benchmark_20260707_111703.json +0 -120
- benchmark/results/benchmark_20260707_111914.json +0 -160
- benchmark/results/benchmark_20260707_112107.json +0 -163
- benchmark/results/benchmark_20260707_150812.json +0 -163
- benchmark/results/benchmark_20260707_163623.json +0 -163
- benchmark/results/benchmark_20260707_163851.json +0 -158
- benchmark/results/benchmark_20260707_164015.json +0 -158
- benchmark/results/benchmark_20260707_164018.json +0 -158
- benchmark/results/benchmark_20260707_164213.json +0 -158
- benchmark/results/benchmark_20260707_181600.json +0 -158
- benchmark/results/benchmark_20260708_034005.json +0 -135
- benchmark/results/benchmark_20260708_034317.json +0 -159
- benchmark/results/benchmark_20260708_034854.json +0 -159
- benchmark/results/benchmark_20260708_035238.json +0 -159
- benchmark/results/benchmark_20260708_035329.json +0 -135
- benchmark/run_benchmark.py +0 -432
- processors/enhancement/__init__.py +0 -12
- processors/enhancement/base.py +0 -92
- processors/enhancement/classical.py +0 -35
- processors/enhancement/groq_guided.py +0 -288
- processors/enhancement/huggingface.py +0 -130
- processors/enhancement/huggingface_api.py +0 -140
- processors/enhancement/pipeline.py +0 -124
- processors/enhancement/real_esrgan.py +0 -153
- processors/enhancement/registry.py +0 -133
- processors/ocr_classification.py +0 -82
- processors/paddleocr_processor.py +0 -157
- requirements.cloud.txt +0 -19
- tests/__init__.py +0 -0
- tests/conftest.py +0 -53
- tests/test_api.py +0 -122
- tests/test_image_enhancer.py +0 -52
- tests/test_line_extractor.py +0 -81
- tests/test_ocr_processor.py +0 -106
- tests/test_svg_generator.py +0 -75
ai_providers.py
DELETED
|
@@ -1,262 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
AI Vision Provider Router
|
| 3 |
-
==========================
|
| 4 |
-
Reads API keys from .env and routes /api/analyze requests to whichever
|
| 5 |
-
provider the caller requests (or the first available one by default).
|
| 6 |
-
|
| 7 |
-
Supported providers
|
| 8 |
-
-------------------
|
| 9 |
-
anthropic — Claude claude-sonnet-4-6 (pip install anthropic)
|
| 10 |
-
openai — GPT-4o (pip install openai)
|
| 11 |
-
gemini — Gemini 2.0 Flash (pip install google-genai)
|
| 12 |
-
groq — Llama 4 Scout (vision) (pip install groq)
|
| 13 |
-
|
| 14 |
-
Adding a new provider
|
| 15 |
-
---------------------
|
| 16 |
-
1. Add its API key variable to .env / .env.example
|
| 17 |
-
2. Add an entry to PROVIDERS below
|
| 18 |
-
3. Implement a _call_<name>() function
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
from __future__ import annotations
|
| 22 |
-
import base64
|
| 23 |
-
import json
|
| 24 |
-
import logging
|
| 25 |
-
import os
|
| 26 |
-
from typing import Optional
|
| 27 |
-
|
| 28 |
-
logger = logging.getLogger(__name__)
|
| 29 |
-
|
| 30 |
-
# ── Prompt sent to every provider ────────────────────────────────────────────
|
| 31 |
-
_ANALYSIS_PROMPT = (
|
| 32 |
-
"You are an expert in architectural drawings and CAD digitisation.\n"
|
| 33 |
-
"Analyse this architectural drawing and respond ONLY with valid JSON "
|
| 34 |
-
"(no markdown fences) using exactly these fields:\n\n"
|
| 35 |
-
"{\n"
|
| 36 |
-
' "drawing_type": "floor plan | elevation | section | detail | site plan | unknown",\n'
|
| 37 |
-
' "scale": "e.g. 1:100, or null if not visible",\n'
|
| 38 |
-
' "elements": ["list of detected architectural elements"],\n'
|
| 39 |
-
' "rooms": ["list of room names if readable"],\n'
|
| 40 |
-
' "quality_score": 1-10,\n'
|
| 41 |
-
' "quality_notes": "brief assessment of drawing condition",\n'
|
| 42 |
-
' "ocr_difficulty": "easy | moderate | hard | very hard",\n'
|
| 43 |
-
' "digitisation_recommendations": ["actionable recommendations"]\n'
|
| 44 |
-
"}"
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
# ── Provider registry ─────────────────────────────────────────────────────────
|
| 48 |
-
# Each entry: { env_key, display_name, model_id, sdk_package }
|
| 49 |
-
PROVIDERS: dict[str, dict] = {
|
| 50 |
-
"anthropic": {
|
| 51 |
-
"env_key": "ANTHROPIC_API_KEY",
|
| 52 |
-
"display_name": "Anthropic Claude",
|
| 53 |
-
"model_id": "claude-sonnet-4-6",
|
| 54 |
-
"sdk_package": "anthropic",
|
| 55 |
-
},
|
| 56 |
-
"openai": {
|
| 57 |
-
"env_key": "OPENAI_API_KEY",
|
| 58 |
-
"display_name": "OpenAI GPT-4o",
|
| 59 |
-
"model_id": "gpt-4o",
|
| 60 |
-
"sdk_package": "openai",
|
| 61 |
-
},
|
| 62 |
-
"gemini": {
|
| 63 |
-
"env_key": "GOOGLE_API_KEY",
|
| 64 |
-
"display_name": "Google Gemini 2.0 Flash",
|
| 65 |
-
"model_id": "gemini-2.0-flash",
|
| 66 |
-
"sdk_package": "google.genai",
|
| 67 |
-
},
|
| 68 |
-
"groq": {
|
| 69 |
-
"env_key": "GROQ_API_KEY",
|
| 70 |
-
"display_name": "Groq Llama 4 Scout",
|
| 71 |
-
"model_id": "meta-llama/llama-4-scout-17b-16e-instruct",
|
| 72 |
-
"sdk_package": "groq",
|
| 73 |
-
},
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
# Priority order when no provider is specified
|
| 77 |
-
_DEFAULT_PRIORITY = ["anthropic", "openai", "groq", "gemini"]
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
# ── Public helpers ────────────────────────────────────────────────────────────
|
| 81 |
-
|
| 82 |
-
def list_providers() -> list[dict]:
|
| 83 |
-
"""Return status of every provider — used by /api/info."""
|
| 84 |
-
result = []
|
| 85 |
-
for name, cfg in PROVIDERS.items():
|
| 86 |
-
key = os.getenv(cfg["env_key"], "").strip()
|
| 87 |
-
result.append({
|
| 88 |
-
"name": name,
|
| 89 |
-
"display_name": cfg["display_name"],
|
| 90 |
-
"model": cfg["model_id"],
|
| 91 |
-
"configured": bool(key),
|
| 92 |
-
})
|
| 93 |
-
return result
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def get_default_provider() -> Optional[str]:
|
| 97 |
-
"""Return the name of the first provider that has a key set."""
|
| 98 |
-
for name in _DEFAULT_PRIORITY:
|
| 99 |
-
key = os.getenv(PROVIDERS[name]["env_key"], "").strip()
|
| 100 |
-
if key:
|
| 101 |
-
return name
|
| 102 |
-
return None
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def analyze(
|
| 106 |
-
image_bytes: bytes,
|
| 107 |
-
filename: str,
|
| 108 |
-
provider_name: Optional[str] = None,
|
| 109 |
-
) -> dict:
|
| 110 |
-
"""
|
| 111 |
-
Run vision analysis on the image using the specified provider.
|
| 112 |
-
Falls back to the first configured provider if none is specified.
|
| 113 |
-
|
| 114 |
-
Returns the parsed JSON analysis dict.
|
| 115 |
-
Raises ValueError if no provider is available.
|
| 116 |
-
Raises RuntimeError on API call failure.
|
| 117 |
-
"""
|
| 118 |
-
name = provider_name or get_default_provider()
|
| 119 |
-
if name is None:
|
| 120 |
-
raise ValueError(
|
| 121 |
-
"No AI provider configured. Add at least one API key to backend/.env"
|
| 122 |
-
)
|
| 123 |
-
if name not in PROVIDERS:
|
| 124 |
-
available = ", ".join(PROVIDERS.keys())
|
| 125 |
-
raise ValueError(f"Unknown provider '{name}'. Choose from: {available}")
|
| 126 |
-
|
| 127 |
-
cfg = PROVIDERS[name]
|
| 128 |
-
api_key = os.getenv(cfg["env_key"], "").strip()
|
| 129 |
-
if not api_key:
|
| 130 |
-
raise ValueError(
|
| 131 |
-
f"Provider '{name}' has no API key set. "
|
| 132 |
-
f"Add {cfg['env_key']} to backend/.env"
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
media_type = _media_type(filename)
|
| 136 |
-
logger.info("AI analysis via %s (%s)", cfg["display_name"], cfg["model_id"])
|
| 137 |
-
|
| 138 |
-
if name == "anthropic":
|
| 139 |
-
raw = _call_anthropic(api_key, cfg["model_id"], image_bytes, media_type)
|
| 140 |
-
elif name == "openai":
|
| 141 |
-
raw = _call_openai(api_key, cfg["model_id"], image_bytes, media_type)
|
| 142 |
-
elif name == "gemini":
|
| 143 |
-
raw = _call_gemini(api_key, cfg["model_id"], image_bytes, media_type)
|
| 144 |
-
elif name == "groq":
|
| 145 |
-
raw = _call_groq(api_key, cfg["model_id"], image_bytes, media_type)
|
| 146 |
-
else:
|
| 147 |
-
raise ValueError(f"No call handler for provider '{name}'")
|
| 148 |
-
|
| 149 |
-
return _parse_response(raw, name)
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
# ── Provider call implementations ─────────────────────────────────────────────
|
| 153 |
-
|
| 154 |
-
def _call_anthropic(api_key: str, model: str, image_bytes: bytes, media_type: str) -> str:
|
| 155 |
-
import anthropic
|
| 156 |
-
client = anthropic.Anthropic(api_key=api_key)
|
| 157 |
-
b64 = base64.b64encode(image_bytes).decode()
|
| 158 |
-
msg = client.messages.create(
|
| 159 |
-
model=model,
|
| 160 |
-
max_tokens=1024,
|
| 161 |
-
messages=[{
|
| 162 |
-
"role": "user",
|
| 163 |
-
"content": [
|
| 164 |
-
{"type": "image", "source": {"type": "base64", "media_type": media_type, "data": b64}},
|
| 165 |
-
{"type": "text", "text": _ANALYSIS_PROMPT},
|
| 166 |
-
],
|
| 167 |
-
}],
|
| 168 |
-
)
|
| 169 |
-
return msg.content[0].text
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
def _call_openai(api_key: str, model: str, image_bytes: bytes, media_type: str) -> str:
|
| 173 |
-
from openai import OpenAI
|
| 174 |
-
client = OpenAI(api_key=api_key)
|
| 175 |
-
b64 = base64.b64encode(image_bytes).decode()
|
| 176 |
-
data_url = f"data:{media_type};base64,{b64}"
|
| 177 |
-
resp = client.chat.completions.create(
|
| 178 |
-
model=model,
|
| 179 |
-
max_tokens=1024,
|
| 180 |
-
messages=[{
|
| 181 |
-
"role": "user",
|
| 182 |
-
"content": [
|
| 183 |
-
{"type": "image_url", "image_url": {"url": data_url}},
|
| 184 |
-
{"type": "text", "text": _ANALYSIS_PROMPT},
|
| 185 |
-
],
|
| 186 |
-
}],
|
| 187 |
-
)
|
| 188 |
-
return resp.choices[0].message.content
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
def _call_gemini(api_key: str, model: str, image_bytes: bytes, media_type: str) -> str:
|
| 192 |
-
from google import genai
|
| 193 |
-
from google.genai import types
|
| 194 |
-
|
| 195 |
-
client = genai.Client(api_key=api_key)
|
| 196 |
-
img_part = types.Part.from_bytes(data=image_bytes, mime_type=media_type)
|
| 197 |
-
resp = client.models.generate_content(
|
| 198 |
-
model=model,
|
| 199 |
-
contents=[img_part, _ANALYSIS_PROMPT],
|
| 200 |
-
config=types.GenerateContentConfig(
|
| 201 |
-
safety_settings=[
|
| 202 |
-
types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE"),
|
| 203 |
-
types.SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="BLOCK_NONE"),
|
| 204 |
-
types.SafetySetting(category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="BLOCK_NONE"),
|
| 205 |
-
types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_NONE"),
|
| 206 |
-
]
|
| 207 |
-
),
|
| 208 |
-
)
|
| 209 |
-
return resp.text
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
def _call_groq(api_key: str, model: str, image_bytes: bytes, media_type: str) -> str:
|
| 213 |
-
import base64, warnings
|
| 214 |
-
import httpx
|
| 215 |
-
from groq import Groq
|
| 216 |
-
|
| 217 |
-
# Corporate proxy uses a self-signed cert — bypass SSL verification
|
| 218 |
-
with warnings.catch_warnings():
|
| 219 |
-
warnings.simplefilter("ignore")
|
| 220 |
-
http_client = httpx.Client(verify=False) # noqa: S501
|
| 221 |
-
client = Groq(api_key=api_key, http_client=http_client)
|
| 222 |
-
|
| 223 |
-
b64 = base64.b64encode(image_bytes).decode()
|
| 224 |
-
data_url = f"data:{media_type};base64,{b64}"
|
| 225 |
-
resp = client.chat.completions.create(
|
| 226 |
-
model=model,
|
| 227 |
-
max_tokens=1024,
|
| 228 |
-
messages=[{
|
| 229 |
-
"role": "user",
|
| 230 |
-
"content": [
|
| 231 |
-
{"type": "image_url", "image_url": {"url": data_url}},
|
| 232 |
-
{"type": "text", "text": _ANALYSIS_PROMPT},
|
| 233 |
-
],
|
| 234 |
-
}],
|
| 235 |
-
)
|
| 236 |
-
return resp.choices[0].message.content
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
# ── Helpers ───────────────────────────────────────────────────────────────────
|
| 240 |
-
|
| 241 |
-
def _media_type(filename: str) -> str:
|
| 242 |
-
fname = (filename or "").lower()
|
| 243 |
-
if fname.endswith(".png"):
|
| 244 |
-
return "image/png"
|
| 245 |
-
if fname.endswith(".webp"):
|
| 246 |
-
return "image/webp"
|
| 247 |
-
return "image/jpeg"
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
def _parse_response(raw: str, provider: str) -> dict:
|
| 251 |
-
text = raw.strip()
|
| 252 |
-
if text.startswith("```"):
|
| 253 |
-
parts = text.split("```")
|
| 254 |
-
text = parts[1]
|
| 255 |
-
if text.startswith("json"):
|
| 256 |
-
text = text[4:]
|
| 257 |
-
text = text.strip()
|
| 258 |
-
try:
|
| 259 |
-
return json.loads(text)
|
| 260 |
-
except json.JSONDecodeError:
|
| 261 |
-
logger.warning("Provider %s returned non-JSON: %s", provider, text[:200])
|
| 262 |
-
return {"raw_response": text}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/__init__.py
DELETED
|
File without changes
|
benchmark/ground_truth.example.json
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"_comment": "Copy this file to ground_truth.json and fill in values for your test images.",
|
| 3 |
-
"_format": {
|
| 4 |
-
"image_filename": {
|
| 5 |
-
"drawing_type": "floor plan | elevation | section | detail | site plan",
|
| 6 |
-
"expected_text": ["list", "of", "key", "labels", "that", "must", "be", "detected"],
|
| 7 |
-
"expected_dimensions": ["3640mm", "2730mm"],
|
| 8 |
-
"expected_room_names": ["Living Room", "Bedroom"],
|
| 9 |
-
"expected_line_count_min": 10,
|
| 10 |
-
"expected_line_count_max": 100,
|
| 11 |
-
"notes": "Any context about this drawing"
|
| 12 |
-
}
|
| 13 |
-
},
|
| 14 |
-
"sample_floor_plan.jpg": {
|
| 15 |
-
"drawing_type": "floor plan",
|
| 16 |
-
"expected_text": ["Living Room", "Bedroom", "Kitchen", "3640", "1:100"],
|
| 17 |
-
"expected_dimensions": [],
|
| 18 |
-
"expected_room_names": ["Living Room", "Bedroom", "Kitchen"],
|
| 19 |
-
"expected_line_count_min": 20,
|
| 20 |
-
"expected_line_count_max": 150,
|
| 21 |
-
"notes": "Standard A3 floor plan, moderate yellowing"
|
| 22 |
-
},
|
| 23 |
-
"sample_elevation.jpg": {
|
| 24 |
-
"drawing_type": "elevation",
|
| 25 |
-
"expected_text": ["GL", "FL", "2F", "1F"],
|
| 26 |
-
"expected_dimensions": [],
|
| 27 |
-
"expected_room_names": [],
|
| 28 |
-
"expected_line_count_min": 15,
|
| 29 |
-
"expected_line_count_max": 80,
|
| 30 |
-
"notes": "South elevation, faded ink"
|
| 31 |
-
}
|
| 32 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/ground_truth.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"_comment": "Ground truth for benchmark/sample_images/*. See ground_truth.example.json for field meanings.",
|
| 3 |
-
|
| 4 |
-
"floor_plan_level1.jpg": {
|
| 5 |
-
"drawing_type": "floor plan",
|
| 6 |
-
"expected_text": ["Bedroom 1", "Bedroom 2", "Kitchen", "Bathroom", "Living Room",
|
| 7 |
-
"3,640 x 3,640", "2,730 x 4,000", "1,820 x 3,000", "8,190 x 4,000",
|
| 8 |
-
"SCALE", "1:100", "FLOOR PLAN", "LEVEL 1"],
|
| 9 |
-
"expected_dimensions": ["3,640 x 3,640", "2,730 x 4,000", "1,820 x 3,000", "8,190 x 4,000"],
|
| 10 |
-
"expected_room_names": ["Bedroom 1", "Bedroom 2", "Kitchen", "Bathroom", "Living Room"],
|
| 11 |
-
"expected_line_count_min": 15,
|
| 12 |
-
"expected_line_count_max": 40,
|
| 13 |
-
"notes": "4-room floor plan with door swings, dimension callouts, and a title block (SCALE / FLOOR PLAN — LEVEL 1). Line count reflects actual wall/curve segments (walls + 2 door-swing arcs, each arc approximated as several short polyline segments), not raw Hough fragments — see line_extractor.py's merge/curve-detection rewrite."
|
| 14 |
-
},
|
| 15 |
-
|
| 16 |
-
"synthetic_floor_plan.png": {
|
| 17 |
-
"drawing_type": "floor plan",
|
| 18 |
-
"expected_text": ["Bedroom", "Living Room", "3640mm", "1:100"],
|
| 19 |
-
"expected_dimensions": ["3640mm"],
|
| 20 |
-
"expected_room_names": ["Bedroom", "Living Room"],
|
| 21 |
-
"expected_line_count_min": 4,
|
| 22 |
-
"expected_line_count_max": 10,
|
| 23 |
-
"notes": "Simple 2-room synthetic floor plan with one dimension callout and a scale label."
|
| 24 |
-
}
|
| 25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/metrics.py
DELETED
|
@@ -1,122 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Accuracy metrics for the architectural drawing benchmark.
|
| 3 |
-
|
| 4 |
-
All functions return float values in [0, 1] unless noted otherwise.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
import re
|
| 9 |
-
from typing import List
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
# ---------------------------------------------------------------------------
|
| 13 |
-
# OCR metrics
|
| 14 |
-
# ---------------------------------------------------------------------------
|
| 15 |
-
|
| 16 |
-
def _normalize(s: str) -> str:
|
| 17 |
-
"""
|
| 18 |
-
Normalize OCR text for comparison so a correctly-read character isn't
|
| 19 |
-
scored as an error just because it differs in glyph from how the
|
| 20 |
-
ground truth happens to be transcribed — e.g. PaddleOCR reads the
|
| 21 |
-
drawing's actual '×' (U+00D7) multiplication sign for dimension
|
| 22 |
-
callouts like "2,730 × 4,000", while ground truth was transcribed with
|
| 23 |
-
a plain ASCII 'x'. Both represent the same text; only the codepoint
|
| 24 |
-
differs.
|
| 25 |
-
"""
|
| 26 |
-
return s.strip().lower().replace("×", "x")
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def character_error_rate(predicted: str, ground_truth: str) -> float:
|
| 30 |
-
"""
|
| 31 |
-
Character Error Rate (CER) — standard metric for OCR quality.
|
| 32 |
-
|
| 33 |
-
CER = edit_distance(predicted, ground_truth) / len(ground_truth)
|
| 34 |
-
|
| 35 |
-
Lower is better. 0.0 = perfect, 1.0 = completely wrong.
|
| 36 |
-
Returns 0.0 if ground_truth is empty.
|
| 37 |
-
"""
|
| 38 |
-
if not ground_truth:
|
| 39 |
-
return 0.0
|
| 40 |
-
predicted, ground_truth = _normalize(predicted), _normalize(ground_truth)
|
| 41 |
-
dist = _levenshtein(predicted, ground_truth)
|
| 42 |
-
return min(dist / len(ground_truth), 1.0)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def word_error_rate(predicted: str, ground_truth: str) -> float:
|
| 46 |
-
"""
|
| 47 |
-
Word Error Rate (WER) — edit distance at word level.
|
| 48 |
-
|
| 49 |
-
WER = edit_distance(pred_words, gt_words) / len(gt_words)
|
| 50 |
-
|
| 51 |
-
Lower is better.
|
| 52 |
-
"""
|
| 53 |
-
pred_words = _normalize(predicted).split()
|
| 54 |
-
gt_words = _normalize(ground_truth).split()
|
| 55 |
-
if not gt_words:
|
| 56 |
-
return 0.0
|
| 57 |
-
dist = _levenshtein(pred_words, gt_words)
|
| 58 |
-
return min(dist / len(gt_words), 1.0)
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def token_recall(predicted_tokens: List[str], expected_tokens: List[str]) -> float:
|
| 62 |
-
"""
|
| 63 |
-
Fraction of expected tokens found anywhere in the predicted token list.
|
| 64 |
-
|
| 65 |
-
Useful for checking that key labels (room names, dimensions) were detected
|
| 66 |
-
without penalising extra detections.
|
| 67 |
-
"""
|
| 68 |
-
if not expected_tokens:
|
| 69 |
-
return 1.0
|
| 70 |
-
pred_set = {_normalize(t) for t in predicted_tokens}
|
| 71 |
-
hits = sum(1 for t in expected_tokens if _normalize(t) in pred_set)
|
| 72 |
-
return hits / len(expected_tokens)
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def token_precision(predicted_tokens: List[str], expected_tokens: List[str]) -> float:
|
| 76 |
-
"""
|
| 77 |
-
Fraction of predicted tokens that appear in the expected list.
|
| 78 |
-
"""
|
| 79 |
-
if not predicted_tokens:
|
| 80 |
-
return 1.0 if not expected_tokens else 0.0
|
| 81 |
-
exp_set = {_normalize(t) for t in expected_tokens}
|
| 82 |
-
hits = sum(1 for t in predicted_tokens if _normalize(t) in exp_set)
|
| 83 |
-
return hits / len(predicted_tokens)
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def f1(precision: float, recall: float) -> float:
|
| 87 |
-
if precision + recall == 0:
|
| 88 |
-
return 0.0
|
| 89 |
-
return 2 * precision * recall / (precision + recall)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
# ---------------------------------------------------------------------------
|
| 93 |
-
# Line detection metrics
|
| 94 |
-
# ---------------------------------------------------------------------------
|
| 95 |
-
|
| 96 |
-
def line_count_accuracy(detected: int, expected_min: int, expected_max: int) -> float:
|
| 97 |
-
"""
|
| 98 |
-
Returns 1.0 if detected count is within the expected range,
|
| 99 |
-
otherwise a linear penalty based on how far outside the range it falls.
|
| 100 |
-
"""
|
| 101 |
-
if expected_min <= detected <= expected_max:
|
| 102 |
-
return 1.0
|
| 103 |
-
midpoint = (expected_min + expected_max) / 2
|
| 104 |
-
deviation = abs(detected - midpoint) / max(midpoint, 1)
|
| 105 |
-
return max(0.0, 1.0 - deviation)
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
# ---------------------------------------------------------------------------
|
| 109 |
-
# Internal helpers
|
| 110 |
-
# ---------------------------------------------------------------------------
|
| 111 |
-
|
| 112 |
-
def _levenshtein(a, b) -> int:
|
| 113 |
-
"""Generic Levenshtein distance — works on strings or lists."""
|
| 114 |
-
m, n = len(a), len(b)
|
| 115 |
-
dp = list(range(n + 1))
|
| 116 |
-
for i in range(1, m + 1):
|
| 117 |
-
prev = dp[:]
|
| 118 |
-
dp[0] = i
|
| 119 |
-
for j in range(1, n + 1):
|
| 120 |
-
cost = 0 if a[i - 1] == b[j - 1] else 1
|
| 121 |
-
dp[j] = min(dp[j] + 1, dp[j - 1] + 1, prev[j - 1] + cost)
|
| 122 |
-
return dp[n]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/after_fix.json
DELETED
|
@@ -1,160 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T11:17:46.470453",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "benchmark/results/after_fix.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.647
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.011,
|
| 25 |
-
"lines_detected": 243,
|
| 26 |
-
"horizontal": 223,
|
| 27 |
-
"vertical": 20,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 1.013,
|
| 33 |
-
"element_count": 17,
|
| 34 |
-
"full_text": "tum ttや5R5R5t5や5や5つOOつつつつつつべつべつべべ\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\nBedroom 2 Living Room\n3,640 x 5,640 8,190 x 4,000\n— oo oo Hr rT HH\nSCALE 1:10 g00mm FLOOR PLAN — LEVEL 1 6,400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 5,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6,400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [],
|
| 52 |
-
"labels": [
|
| 53 |
-
"oo",
|
| 54 |
-
"rT",
|
| 55 |
-
"SCALE",
|
| 56 |
-
"g00mm",
|
| 57 |
-
"FLOOR PLAN",
|
| 58 |
-
"LEVEL 1"
|
| 59 |
-
]
|
| 60 |
-
}
|
| 61 |
-
},
|
| 62 |
-
"svg": {
|
| 63 |
-
"success": true,
|
| 64 |
-
"time_s": 0.006,
|
| 65 |
-
"svg_size_bytes": 21209
|
| 66 |
-
}
|
| 67 |
-
},
|
| 68 |
-
"errors": [],
|
| 69 |
-
"total_time_s": 1.677,
|
| 70 |
-
"accuracy": {
|
| 71 |
-
"ocr_cer": 1.0,
|
| 72 |
-
"ocr_wer": 0.8462,
|
| 73 |
-
"ocr_token_recall": 0.9231,
|
| 74 |
-
"ocr_token_precision": 0.7059,
|
| 75 |
-
"ocr_token_f1": 0.8,
|
| 76 |
-
"room_name_recall": 1.0,
|
| 77 |
-
"line_count_accuracy": 1.0
|
| 78 |
-
}
|
| 79 |
-
},
|
| 80 |
-
{
|
| 81 |
-
"filename": "synthetic_floor_plan.png",
|
| 82 |
-
"size_bytes": 12406,
|
| 83 |
-
"dimensions": {
|
| 84 |
-
"width": 1200,
|
| 85 |
-
"height": 900
|
| 86 |
-
},
|
| 87 |
-
"stages": {
|
| 88 |
-
"enhancement": {
|
| 89 |
-
"success": true,
|
| 90 |
-
"time_s": 0.567
|
| 91 |
-
},
|
| 92 |
-
"line_extraction": {
|
| 93 |
-
"success": true,
|
| 94 |
-
"time_s": 0.005,
|
| 95 |
-
"lines_detected": 26,
|
| 96 |
-
"horizontal": 20,
|
| 97 |
-
"vertical": 6,
|
| 98 |
-
"diagonal": 0
|
| 99 |
-
},
|
| 100 |
-
"ocr": {
|
| 101 |
-
"success": true,
|
| 102 |
-
"time_s": 0.381,
|
| 103 |
-
"element_count": 3,
|
| 104 |
-
"full_text": "ーー ーー\n3640mm\nLiving Room\n1:100",
|
| 105 |
-
"classified": {
|
| 106 |
-
"dimensions": [
|
| 107 |
-
"3640mm"
|
| 108 |
-
],
|
| 109 |
-
"room_names": [
|
| 110 |
-
"Living Room"
|
| 111 |
-
],
|
| 112 |
-
"numbers": [],
|
| 113 |
-
"labels": [
|
| 114 |
-
"1:100"
|
| 115 |
-
]
|
| 116 |
-
}
|
| 117 |
-
},
|
| 118 |
-
"svg": {
|
| 119 |
-
"success": true,
|
| 120 |
-
"time_s": 0.001,
|
| 121 |
-
"svg_size_bytes": 2698
|
| 122 |
-
}
|
| 123 |
-
},
|
| 124 |
-
"errors": [],
|
| 125 |
-
"total_time_s": 0.954,
|
| 126 |
-
"accuracy": {
|
| 127 |
-
"ocr_cer": 0.7812,
|
| 128 |
-
"ocr_wer": 0.8,
|
| 129 |
-
"ocr_token_recall": 0.75,
|
| 130 |
-
"ocr_token_precision": 1.0,
|
| 131 |
-
"ocr_token_f1": 0.8571,
|
| 132 |
-
"room_name_recall": 0.5,
|
| 133 |
-
"line_count_accuracy": 1.0
|
| 134 |
-
}
|
| 135 |
-
}
|
| 136 |
-
],
|
| 137 |
-
"aggregate": {
|
| 138 |
-
"avg_ocr_cer": 0.8906,
|
| 139 |
-
"min_ocr_cer": 0.7812,
|
| 140 |
-
"max_ocr_cer": 1.0,
|
| 141 |
-
"avg_ocr_wer": 0.8231,
|
| 142 |
-
"min_ocr_wer": 0.8,
|
| 143 |
-
"max_ocr_wer": 0.8462,
|
| 144 |
-
"avg_ocr_token_recall": 0.8366,
|
| 145 |
-
"min_ocr_token_recall": 0.75,
|
| 146 |
-
"max_ocr_token_recall": 0.9231,
|
| 147 |
-
"avg_ocr_token_precision": 0.8529,
|
| 148 |
-
"min_ocr_token_precision": 0.7059,
|
| 149 |
-
"max_ocr_token_precision": 1.0,
|
| 150 |
-
"avg_ocr_token_f1": 0.8286,
|
| 151 |
-
"min_ocr_token_f1": 0.8,
|
| 152 |
-
"max_ocr_token_f1": 0.8571,
|
| 153 |
-
"avg_room_name_recall": 0.75,
|
| 154 |
-
"min_room_name_recall": 0.5,
|
| 155 |
-
"max_room_name_recall": 1.0,
|
| 156 |
-
"avg_line_count_accuracy": 1.0,
|
| 157 |
-
"min_line_count_accuracy": 1.0,
|
| 158 |
-
"max_line_count_accuracy": 1.0
|
| 159 |
-
}
|
| 160 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260629_114932.json
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-06-29T11:49:32.822180",
|
| 4 |
-
"image_count": 1,
|
| 5 |
-
"ground_truth": null,
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260629_114932.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "synthetic_floor_plan.png",
|
| 12 |
-
"size_bytes": 12406,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1200,
|
| 15 |
-
"height": 900
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.149
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.004,
|
| 25 |
-
"lines_detected": 20,
|
| 26 |
-
"horizontal": 14,
|
| 27 |
-
"vertical": 6,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.247,
|
| 33 |
-
"element_count": 5,
|
| 34 |
-
"full_text": "_ _\n3640mm\nLiving Room\n1:100",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3640mm"
|
| 38 |
-
],
|
| 39 |
-
"room_names": [
|
| 40 |
-
"Living",
|
| 41 |
-
"Room"
|
| 42 |
-
],
|
| 43 |
-
"numbers": [],
|
| 44 |
-
"labels": [
|
| 45 |
-
"_",
|
| 46 |
-
"1:100"
|
| 47 |
-
]
|
| 48 |
-
}
|
| 49 |
-
},
|
| 50 |
-
"svg": {
|
| 51 |
-
"success": true,
|
| 52 |
-
"time_s": 0.001,
|
| 53 |
-
"svg_size_bytes": 2307
|
| 54 |
-
}
|
| 55 |
-
},
|
| 56 |
-
"errors": [],
|
| 57 |
-
"total_time_s": 0.401
|
| 58 |
-
}
|
| 59 |
-
],
|
| 60 |
-
"aggregate": {}
|
| 61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260629_115100.json
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-06-29T11:51:01.173922",
|
| 4 |
-
"image_count": 1,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth_test.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260629_115100.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "synthetic_floor_plan.png",
|
| 12 |
-
"size_bytes": 12406,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1200,
|
| 15 |
-
"height": 900
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.115
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.004,
|
| 25 |
-
"lines_detected": 20,
|
| 26 |
-
"horizontal": 14,
|
| 27 |
-
"vertical": 6,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.255,
|
| 33 |
-
"element_count": 5,
|
| 34 |
-
"full_text": "_ _\n3640mm\nLiving Room\n1:100",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3640mm"
|
| 38 |
-
],
|
| 39 |
-
"room_names": [
|
| 40 |
-
"Living",
|
| 41 |
-
"Room"
|
| 42 |
-
],
|
| 43 |
-
"numbers": [],
|
| 44 |
-
"labels": [
|
| 45 |
-
"_",
|
| 46 |
-
"1:100"
|
| 47 |
-
]
|
| 48 |
-
}
|
| 49 |
-
},
|
| 50 |
-
"svg": {
|
| 51 |
-
"success": true,
|
| 52 |
-
"time_s": 0.001,
|
| 53 |
-
"svg_size_bytes": 2307
|
| 54 |
-
}
|
| 55 |
-
},
|
| 56 |
-
"errors": [],
|
| 57 |
-
"total_time_s": 0.375,
|
| 58 |
-
"accuracy": {
|
| 59 |
-
"ocr_cer": 0.75,
|
| 60 |
-
"ocr_wer": 1.0,
|
| 61 |
-
"ocr_token_recall": 0.5,
|
| 62 |
-
"ocr_token_precision": 0.4,
|
| 63 |
-
"ocr_token_f1": 0.4444,
|
| 64 |
-
"room_name_recall": 0.0,
|
| 65 |
-
"line_count_accuracy": 1.0
|
| 66 |
-
}
|
| 67 |
-
}
|
| 68 |
-
],
|
| 69 |
-
"aggregate": {
|
| 70 |
-
"avg_ocr_cer": 0.75,
|
| 71 |
-
"min_ocr_cer": 0.75,
|
| 72 |
-
"max_ocr_cer": 0.75,
|
| 73 |
-
"avg_ocr_wer": 1.0,
|
| 74 |
-
"min_ocr_wer": 1.0,
|
| 75 |
-
"max_ocr_wer": 1.0,
|
| 76 |
-
"avg_ocr_token_recall": 0.5,
|
| 77 |
-
"min_ocr_token_recall": 0.5,
|
| 78 |
-
"max_ocr_token_recall": 0.5,
|
| 79 |
-
"avg_ocr_token_precision": 0.4,
|
| 80 |
-
"min_ocr_token_precision": 0.4,
|
| 81 |
-
"max_ocr_token_precision": 0.4,
|
| 82 |
-
"avg_ocr_token_f1": 0.4444,
|
| 83 |
-
"min_ocr_token_f1": 0.4444,
|
| 84 |
-
"max_ocr_token_f1": 0.4444,
|
| 85 |
-
"avg_room_name_recall": 0.0,
|
| 86 |
-
"min_room_name_recall": 0.0,
|
| 87 |
-
"max_room_name_recall": 0.0,
|
| 88 |
-
"avg_line_count_accuracy": 1.0,
|
| 89 |
-
"min_line_count_accuracy": 1.0,
|
| 90 |
-
"max_line_count_accuracy": 1.0
|
| 91 |
-
}
|
| 92 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_110502.json
DELETED
|
@@ -1,139 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T11:05:05.449218",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": null,
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_110502.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.627
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.011,
|
| 25 |
-
"lines_detected": 242,
|
| 26 |
-
"horizontal": 221,
|
| 27 |
-
"vertical": 21,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 1.064,
|
| 33 |
-
"element_count": 32,
|
| 34 |
-
"full_text": "a1 a\n\nBedroom 1 Kitchen Bathroom\n\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n\n3,640 x 5,640 8,190 x 4,000\nmm\nSCALE 11100600mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"6.400mm"
|
| 38 |
-
],
|
| 39 |
-
"room_names": [
|
| 40 |
-
"Bedroom",
|
| 41 |
-
"Kitchen",
|
| 42 |
-
"Bathroom",
|
| 43 |
-
"Bedroom",
|
| 44 |
-
"Living",
|
| 45 |
-
"Room",
|
| 46 |
-
"SCALE",
|
| 47 |
-
"FLOOR",
|
| 48 |
-
"PLAN",
|
| 49 |
-
"LEVEL"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [
|
| 52 |
-
"1",
|
| 53 |
-
"2",
|
| 54 |
-
"1"
|
| 55 |
-
],
|
| 56 |
-
"labels": [
|
| 57 |
-
"a1",
|
| 58 |
-
"a",
|
| 59 |
-
"3,640",
|
| 60 |
-
"x",
|
| 61 |
-
"3,640",
|
| 62 |
-
"2,730",
|
| 63 |
-
"x",
|
| 64 |
-
"4,000",
|
| 65 |
-
"1,820",
|
| 66 |
-
"x",
|
| 67 |
-
"3,000",
|
| 68 |
-
"3,640",
|
| 69 |
-
"x",
|
| 70 |
-
"5,640",
|
| 71 |
-
"8,190",
|
| 72 |
-
"x",
|
| 73 |
-
"4,000",
|
| 74 |
-
"—"
|
| 75 |
-
]
|
| 76 |
-
}
|
| 77 |
-
},
|
| 78 |
-
"svg": {
|
| 79 |
-
"success": true,
|
| 80 |
-
"time_s": 0.006,
|
| 81 |
-
"svg_size_bytes": 22576
|
| 82 |
-
}
|
| 83 |
-
},
|
| 84 |
-
"errors": [],
|
| 85 |
-
"total_time_s": 1.708
|
| 86 |
-
},
|
| 87 |
-
{
|
| 88 |
-
"filename": "synthetic_floor_plan.png",
|
| 89 |
-
"size_bytes": 12406,
|
| 90 |
-
"dimensions": {
|
| 91 |
-
"width": 1200,
|
| 92 |
-
"height": 900
|
| 93 |
-
},
|
| 94 |
-
"stages": {
|
| 95 |
-
"enhancement": {
|
| 96 |
-
"success": true,
|
| 97 |
-
"time_s": 0.542
|
| 98 |
-
},
|
| 99 |
-
"line_extraction": {
|
| 100 |
-
"success": true,
|
| 101 |
-
"time_s": 0.006,
|
| 102 |
-
"lines_detected": 26,
|
| 103 |
-
"horizontal": 20,
|
| 104 |
-
"vertical": 6,
|
| 105 |
-
"diagonal": 0
|
| 106 |
-
},
|
| 107 |
-
"ocr": {
|
| 108 |
-
"success": true,
|
| 109 |
-
"time_s": 0.38,
|
| 110 |
-
"element_count": 6,
|
| 111 |
-
"full_text": "ーー ーー\n3640mm\nLiving Room\n1:100",
|
| 112 |
-
"classified": {
|
| 113 |
-
"dimensions": [
|
| 114 |
-
"3640mm"
|
| 115 |
-
],
|
| 116 |
-
"room_names": [
|
| 117 |
-
"Living",
|
| 118 |
-
"Room"
|
| 119 |
-
],
|
| 120 |
-
"numbers": [],
|
| 121 |
-
"labels": [
|
| 122 |
-
"ーー",
|
| 123 |
-
"ーー",
|
| 124 |
-
"1:100"
|
| 125 |
-
]
|
| 126 |
-
}
|
| 127 |
-
},
|
| 128 |
-
"svg": {
|
| 129 |
-
"success": true,
|
| 130 |
-
"time_s": 0.001,
|
| 131 |
-
"svg_size_bytes": 2982
|
| 132 |
-
}
|
| 133 |
-
},
|
| 134 |
-
"errors": [],
|
| 135 |
-
"total_time_s": 0.929
|
| 136 |
-
}
|
| 137 |
-
],
|
| 138 |
-
"aggregate": {}
|
| 139 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_111530.json
DELETED
|
@@ -1,121 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T11:15:32.885738",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": null,
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_111530.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.635
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.01,
|
| 25 |
-
"lines_detected": 243,
|
| 26 |
-
"horizontal": 223,
|
| 27 |
-
"vertical": 20,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.982,
|
| 33 |
-
"element_count": 18,
|
| 34 |
-
"full_text": "tum ttや5R5R5t5や5や5つOOつつつつつつべつべつべべ\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\nBedroom 2 Living Room\n3,640 x 5,640 8,190 x 4,000\n— oo oo Hr rT HH\nSCALE 1:10 g00mm FLOOR PLAN — LEVEL 1 6,400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 5,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6,400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [],
|
| 52 |
-
"labels": [
|
| 53 |
-
"5 つつ つつ べつ べつ",
|
| 54 |
-
"oo",
|
| 55 |
-
"rT",
|
| 56 |
-
"SCALE",
|
| 57 |
-
"g00mm",
|
| 58 |
-
"FLOOR PLAN",
|
| 59 |
-
"LEVEL 1"
|
| 60 |
-
]
|
| 61 |
-
}
|
| 62 |
-
},
|
| 63 |
-
"svg": {
|
| 64 |
-
"success": true,
|
| 65 |
-
"time_s": 0.005,
|
| 66 |
-
"svg_size_bytes": 21209
|
| 67 |
-
}
|
| 68 |
-
},
|
| 69 |
-
"errors": [],
|
| 70 |
-
"total_time_s": 1.632
|
| 71 |
-
},
|
| 72 |
-
{
|
| 73 |
-
"filename": "synthetic_floor_plan.png",
|
| 74 |
-
"size_bytes": 12406,
|
| 75 |
-
"dimensions": {
|
| 76 |
-
"width": 1200,
|
| 77 |
-
"height": 900
|
| 78 |
-
},
|
| 79 |
-
"stages": {
|
| 80 |
-
"enhancement": {
|
| 81 |
-
"success": true,
|
| 82 |
-
"time_s": 0.553
|
| 83 |
-
},
|
| 84 |
-
"line_extraction": {
|
| 85 |
-
"success": true,
|
| 86 |
-
"time_s": 0.005,
|
| 87 |
-
"lines_detected": 26,
|
| 88 |
-
"horizontal": 20,
|
| 89 |
-
"vertical": 6,
|
| 90 |
-
"diagonal": 0
|
| 91 |
-
},
|
| 92 |
-
"ocr": {
|
| 93 |
-
"success": true,
|
| 94 |
-
"time_s": 0.379,
|
| 95 |
-
"element_count": 3,
|
| 96 |
-
"full_text": "ーー ーー\n3640mm\nLiving Room\n1:100",
|
| 97 |
-
"classified": {
|
| 98 |
-
"dimensions": [
|
| 99 |
-
"3640mm"
|
| 100 |
-
],
|
| 101 |
-
"room_names": [
|
| 102 |
-
"Living Room"
|
| 103 |
-
],
|
| 104 |
-
"numbers": [],
|
| 105 |
-
"labels": [
|
| 106 |
-
"1:100"
|
| 107 |
-
]
|
| 108 |
-
}
|
| 109 |
-
},
|
| 110 |
-
"svg": {
|
| 111 |
-
"success": true,
|
| 112 |
-
"time_s": 0.001,
|
| 113 |
-
"svg_size_bytes": 2698
|
| 114 |
-
}
|
| 115 |
-
},
|
| 116 |
-
"errors": [],
|
| 117 |
-
"total_time_s": 0.938
|
| 118 |
-
}
|
| 119 |
-
],
|
| 120 |
-
"aggregate": {}
|
| 121 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_111703.json
DELETED
|
@@ -1,120 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T11:17:05.935392",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": null,
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_111703.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.635
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.01,
|
| 25 |
-
"lines_detected": 243,
|
| 26 |
-
"horizontal": 223,
|
| 27 |
-
"vertical": 20,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.973,
|
| 33 |
-
"element_count": 17,
|
| 34 |
-
"full_text": "tum ttや5R5R5t5や5や5つOOつつつつつつべつべつべべ\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\nBedroom 2 Living Room\n3,640 x 5,640 8,190 x 4,000\n— oo oo Hr rT HH\nSCALE 1:10 g00mm FLOOR PLAN — LEVEL 1 6,400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 5,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6,400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [],
|
| 52 |
-
"labels": [
|
| 53 |
-
"oo",
|
| 54 |
-
"rT",
|
| 55 |
-
"SCALE",
|
| 56 |
-
"g00mm",
|
| 57 |
-
"FLOOR PLAN",
|
| 58 |
-
"LEVEL 1"
|
| 59 |
-
]
|
| 60 |
-
}
|
| 61 |
-
},
|
| 62 |
-
"svg": {
|
| 63 |
-
"success": true,
|
| 64 |
-
"time_s": 0.005,
|
| 65 |
-
"svg_size_bytes": 21209
|
| 66 |
-
}
|
| 67 |
-
},
|
| 68 |
-
"errors": [],
|
| 69 |
-
"total_time_s": 1.623
|
| 70 |
-
},
|
| 71 |
-
{
|
| 72 |
-
"filename": "synthetic_floor_plan.png",
|
| 73 |
-
"size_bytes": 12406,
|
| 74 |
-
"dimensions": {
|
| 75 |
-
"width": 1200,
|
| 76 |
-
"height": 900
|
| 77 |
-
},
|
| 78 |
-
"stages": {
|
| 79 |
-
"enhancement": {
|
| 80 |
-
"success": true,
|
| 81 |
-
"time_s": 0.549
|
| 82 |
-
},
|
| 83 |
-
"line_extraction": {
|
| 84 |
-
"success": true,
|
| 85 |
-
"time_s": 0.005,
|
| 86 |
-
"lines_detected": 26,
|
| 87 |
-
"horizontal": 20,
|
| 88 |
-
"vertical": 6,
|
| 89 |
-
"diagonal": 0
|
| 90 |
-
},
|
| 91 |
-
"ocr": {
|
| 92 |
-
"success": true,
|
| 93 |
-
"time_s": 0.375,
|
| 94 |
-
"element_count": 3,
|
| 95 |
-
"full_text": "ーー ーー\n3640mm\nLiving Room\n1:100",
|
| 96 |
-
"classified": {
|
| 97 |
-
"dimensions": [
|
| 98 |
-
"3640mm"
|
| 99 |
-
],
|
| 100 |
-
"room_names": [
|
| 101 |
-
"Living Room"
|
| 102 |
-
],
|
| 103 |
-
"numbers": [],
|
| 104 |
-
"labels": [
|
| 105 |
-
"1:100"
|
| 106 |
-
]
|
| 107 |
-
}
|
| 108 |
-
},
|
| 109 |
-
"svg": {
|
| 110 |
-
"success": true,
|
| 111 |
-
"time_s": 0.001,
|
| 112 |
-
"svg_size_bytes": 2698
|
| 113 |
-
}
|
| 114 |
-
},
|
| 115 |
-
"errors": [],
|
| 116 |
-
"total_time_s": 0.93
|
| 117 |
-
}
|
| 118 |
-
],
|
| 119 |
-
"aggregate": {}
|
| 120 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_111914.json
DELETED
|
@@ -1,160 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T11:19:17.139313",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_111914.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.636
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.01,
|
| 25 |
-
"lines_detected": 243,
|
| 26 |
-
"horizontal": 223,
|
| 27 |
-
"vertical": 20,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.565,
|
| 33 |
-
"element_count": 17,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6.400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [
|
| 52 |
-
"7",
|
| 53 |
-
"1"
|
| 54 |
-
],
|
| 55 |
-
"labels": [
|
| 56 |
-
"{",
|
| 57 |
-
"\"SCALE",
|
| 58 |
-
"FLOOR PLAN",
|
| 59 |
-
"LEVEL 1"
|
| 60 |
-
]
|
| 61 |
-
}
|
| 62 |
-
},
|
| 63 |
-
"svg": {
|
| 64 |
-
"success": true,
|
| 65 |
-
"time_s": 0.006,
|
| 66 |
-
"svg_size_bytes": 21483
|
| 67 |
-
}
|
| 68 |
-
},
|
| 69 |
-
"errors": [],
|
| 70 |
-
"total_time_s": 1.217,
|
| 71 |
-
"accuracy": {
|
| 72 |
-
"ocr_cer": 1.0,
|
| 73 |
-
"ocr_wer": 0.7308,
|
| 74 |
-
"ocr_token_recall": 0.8462,
|
| 75 |
-
"ocr_token_precision": 0.7059,
|
| 76 |
-
"ocr_token_f1": 0.7697,
|
| 77 |
-
"room_name_recall": 1.0,
|
| 78 |
-
"line_count_accuracy": 1.0
|
| 79 |
-
}
|
| 80 |
-
},
|
| 81 |
-
{
|
| 82 |
-
"filename": "synthetic_floor_plan.png",
|
| 83 |
-
"size_bytes": 12406,
|
| 84 |
-
"dimensions": {
|
| 85 |
-
"width": 1200,
|
| 86 |
-
"height": 900
|
| 87 |
-
},
|
| 88 |
-
"stages": {
|
| 89 |
-
"enhancement": {
|
| 90 |
-
"success": true,
|
| 91 |
-
"time_s": 0.552
|
| 92 |
-
},
|
| 93 |
-
"line_extraction": {
|
| 94 |
-
"success": true,
|
| 95 |
-
"time_s": 0.005,
|
| 96 |
-
"lines_detected": 26,
|
| 97 |
-
"horizontal": 20,
|
| 98 |
-
"vertical": 6,
|
| 99 |
-
"diagonal": 0
|
| 100 |
-
},
|
| 101 |
-
"ocr": {
|
| 102 |
-
"success": true,
|
| 103 |
-
"time_s": 0.353,
|
| 104 |
-
"element_count": 3,
|
| 105 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 106 |
-
"classified": {
|
| 107 |
-
"dimensions": [],
|
| 108 |
-
"room_names": [
|
| 109 |
-
"Bedroom",
|
| 110 |
-
"Living Room"
|
| 111 |
-
],
|
| 112 |
-
"numbers": [],
|
| 113 |
-
"labels": [
|
| 114 |
-
"1:100"
|
| 115 |
-
]
|
| 116 |
-
}
|
| 117 |
-
},
|
| 118 |
-
"svg": {
|
| 119 |
-
"success": true,
|
| 120 |
-
"time_s": 0.001,
|
| 121 |
-
"svg_size_bytes": 2699
|
| 122 |
-
}
|
| 123 |
-
},
|
| 124 |
-
"errors": [],
|
| 125 |
-
"total_time_s": 0.911,
|
| 126 |
-
"accuracy": {
|
| 127 |
-
"ocr_cer": 0.3125,
|
| 128 |
-
"ocr_wer": 0.2,
|
| 129 |
-
"ocr_token_recall": 0.75,
|
| 130 |
-
"ocr_token_precision": 1.0,
|
| 131 |
-
"ocr_token_f1": 0.8571,
|
| 132 |
-
"room_name_recall": 1.0,
|
| 133 |
-
"line_count_accuracy": 1.0
|
| 134 |
-
}
|
| 135 |
-
}
|
| 136 |
-
],
|
| 137 |
-
"aggregate": {
|
| 138 |
-
"avg_ocr_cer": 0.6562,
|
| 139 |
-
"min_ocr_cer": 0.3125,
|
| 140 |
-
"max_ocr_cer": 1.0,
|
| 141 |
-
"avg_ocr_wer": 0.4654,
|
| 142 |
-
"min_ocr_wer": 0.2,
|
| 143 |
-
"max_ocr_wer": 0.7308,
|
| 144 |
-
"avg_ocr_token_recall": 0.7981,
|
| 145 |
-
"min_ocr_token_recall": 0.75,
|
| 146 |
-
"max_ocr_token_recall": 0.8462,
|
| 147 |
-
"avg_ocr_token_precision": 0.8529,
|
| 148 |
-
"min_ocr_token_precision": 0.7059,
|
| 149 |
-
"max_ocr_token_precision": 1.0,
|
| 150 |
-
"avg_ocr_token_f1": 0.8134,
|
| 151 |
-
"min_ocr_token_f1": 0.7697,
|
| 152 |
-
"max_ocr_token_f1": 0.8571,
|
| 153 |
-
"avg_room_name_recall": 1.0,
|
| 154 |
-
"min_room_name_recall": 1.0,
|
| 155 |
-
"max_room_name_recall": 1.0,
|
| 156 |
-
"avg_line_count_accuracy": 1.0,
|
| 157 |
-
"min_line_count_accuracy": 1.0,
|
| 158 |
-
"max_line_count_accuracy": 1.0
|
| 159 |
-
}
|
| 160 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_112107.json
DELETED
|
@@ -1,163 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T11:21:10.382211",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_112107.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.627
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.01,
|
| 25 |
-
"lines_detected": 243,
|
| 26 |
-
"horizontal": 223,
|
| 27 |
-
"vertical": 20,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.843,
|
| 33 |
-
"element_count": 18,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6.400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [
|
| 52 |
-
"7",
|
| 53 |
-
"1"
|
| 54 |
-
],
|
| 55 |
-
"labels": [
|
| 56 |
-
"{",
|
| 57 |
-
"\"SCALE",
|
| 58 |
-
"FLOOR PLAN",
|
| 59 |
-
"LEVEL 1",
|
| 60 |
-
"a"
|
| 61 |
-
]
|
| 62 |
-
}
|
| 63 |
-
},
|
| 64 |
-
"svg": {
|
| 65 |
-
"success": true,
|
| 66 |
-
"time_s": 0.005,
|
| 67 |
-
"svg_size_bytes": 21575
|
| 68 |
-
}
|
| 69 |
-
},
|
| 70 |
-
"errors": [],
|
| 71 |
-
"total_time_s": 1.485,
|
| 72 |
-
"accuracy": {
|
| 73 |
-
"ocr_cer": 1.0,
|
| 74 |
-
"ocr_wer": 0.7308,
|
| 75 |
-
"ocr_token_recall": 0.8462,
|
| 76 |
-
"ocr_token_precision": 0.6667,
|
| 77 |
-
"ocr_token_f1": 0.7458,
|
| 78 |
-
"room_name_recall": 1.0,
|
| 79 |
-
"line_count_accuracy": 1.0
|
| 80 |
-
}
|
| 81 |
-
},
|
| 82 |
-
{
|
| 83 |
-
"filename": "synthetic_floor_plan.png",
|
| 84 |
-
"size_bytes": 12406,
|
| 85 |
-
"dimensions": {
|
| 86 |
-
"width": 1200,
|
| 87 |
-
"height": 900
|
| 88 |
-
},
|
| 89 |
-
"stages": {
|
| 90 |
-
"enhancement": {
|
| 91 |
-
"success": true,
|
| 92 |
-
"time_s": 0.549
|
| 93 |
-
},
|
| 94 |
-
"line_extraction": {
|
| 95 |
-
"success": true,
|
| 96 |
-
"time_s": 0.005,
|
| 97 |
-
"lines_detected": 26,
|
| 98 |
-
"horizontal": 20,
|
| 99 |
-
"vertical": 6,
|
| 100 |
-
"diagonal": 0
|
| 101 |
-
},
|
| 102 |
-
"ocr": {
|
| 103 |
-
"success": true,
|
| 104 |
-
"time_s": 0.532,
|
| 105 |
-
"element_count": 4,
|
| 106 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 107 |
-
"classified": {
|
| 108 |
-
"dimensions": [
|
| 109 |
-
"3640mm"
|
| 110 |
-
],
|
| 111 |
-
"room_names": [
|
| 112 |
-
"Bedroom",
|
| 113 |
-
"Living Room"
|
| 114 |
-
],
|
| 115 |
-
"numbers": [],
|
| 116 |
-
"labels": [
|
| 117 |
-
"1:100"
|
| 118 |
-
]
|
| 119 |
-
}
|
| 120 |
-
},
|
| 121 |
-
"svg": {
|
| 122 |
-
"success": true,
|
| 123 |
-
"time_s": 0.001,
|
| 124 |
-
"svg_size_bytes": 2796
|
| 125 |
-
}
|
| 126 |
-
},
|
| 127 |
-
"errors": [],
|
| 128 |
-
"total_time_s": 1.087,
|
| 129 |
-
"accuracy": {
|
| 130 |
-
"ocr_cer": 0.3125,
|
| 131 |
-
"ocr_wer": 0.2,
|
| 132 |
-
"ocr_token_recall": 1.0,
|
| 133 |
-
"ocr_token_precision": 1.0,
|
| 134 |
-
"ocr_token_f1": 1.0,
|
| 135 |
-
"room_name_recall": 1.0,
|
| 136 |
-
"line_count_accuracy": 1.0
|
| 137 |
-
}
|
| 138 |
-
}
|
| 139 |
-
],
|
| 140 |
-
"aggregate": {
|
| 141 |
-
"avg_ocr_cer": 0.6562,
|
| 142 |
-
"min_ocr_cer": 0.3125,
|
| 143 |
-
"max_ocr_cer": 1.0,
|
| 144 |
-
"avg_ocr_wer": 0.4654,
|
| 145 |
-
"min_ocr_wer": 0.2,
|
| 146 |
-
"max_ocr_wer": 0.7308,
|
| 147 |
-
"avg_ocr_token_recall": 0.9231,
|
| 148 |
-
"min_ocr_token_recall": 0.8462,
|
| 149 |
-
"max_ocr_token_recall": 1.0,
|
| 150 |
-
"avg_ocr_token_precision": 0.8334,
|
| 151 |
-
"min_ocr_token_precision": 0.6667,
|
| 152 |
-
"max_ocr_token_precision": 1.0,
|
| 153 |
-
"avg_ocr_token_f1": 0.8729,
|
| 154 |
-
"min_ocr_token_f1": 0.7458,
|
| 155 |
-
"max_ocr_token_f1": 1.0,
|
| 156 |
-
"avg_room_name_recall": 1.0,
|
| 157 |
-
"min_room_name_recall": 1.0,
|
| 158 |
-
"max_room_name_recall": 1.0,
|
| 159 |
-
"avg_line_count_accuracy": 1.0,
|
| 160 |
-
"min_line_count_accuracy": 1.0,
|
| 161 |
-
"max_line_count_accuracy": 1.0
|
| 162 |
-
}
|
| 163 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_150812.json
DELETED
|
@@ -1,163 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T15:08:17.169789",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_150812.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.979
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.013,
|
| 25 |
-
"lines_detected": 243,
|
| 26 |
-
"horizontal": 223,
|
| 27 |
-
"vertical": 20,
|
| 28 |
-
"diagonal": 0
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 2.075,
|
| 33 |
-
"element_count": 18,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6.400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [
|
| 52 |
-
"7",
|
| 53 |
-
"1"
|
| 54 |
-
],
|
| 55 |
-
"labels": [
|
| 56 |
-
"{",
|
| 57 |
-
"\"SCALE",
|
| 58 |
-
"FLOOR PLAN",
|
| 59 |
-
"LEVEL 1",
|
| 60 |
-
"a"
|
| 61 |
-
]
|
| 62 |
-
}
|
| 63 |
-
},
|
| 64 |
-
"svg": {
|
| 65 |
-
"success": true,
|
| 66 |
-
"time_s": 0.007,
|
| 67 |
-
"svg_size_bytes": 21575
|
| 68 |
-
}
|
| 69 |
-
},
|
| 70 |
-
"errors": [],
|
| 71 |
-
"total_time_s": 3.074,
|
| 72 |
-
"accuracy": {
|
| 73 |
-
"ocr_cer": 1.0,
|
| 74 |
-
"ocr_wer": 0.7308,
|
| 75 |
-
"ocr_token_recall": 0.8462,
|
| 76 |
-
"ocr_token_precision": 0.6667,
|
| 77 |
-
"ocr_token_f1": 0.7458,
|
| 78 |
-
"room_name_recall": 1.0,
|
| 79 |
-
"line_count_accuracy": 1.0
|
| 80 |
-
}
|
| 81 |
-
},
|
| 82 |
-
{
|
| 83 |
-
"filename": "synthetic_floor_plan.png",
|
| 84 |
-
"size_bytes": 12406,
|
| 85 |
-
"dimensions": {
|
| 86 |
-
"width": 1200,
|
| 87 |
-
"height": 900
|
| 88 |
-
},
|
| 89 |
-
"stages": {
|
| 90 |
-
"enhancement": {
|
| 91 |
-
"success": true,
|
| 92 |
-
"time_s": 0.626
|
| 93 |
-
},
|
| 94 |
-
"line_extraction": {
|
| 95 |
-
"success": true,
|
| 96 |
-
"time_s": 0.006,
|
| 97 |
-
"lines_detected": 26,
|
| 98 |
-
"horizontal": 20,
|
| 99 |
-
"vertical": 6,
|
| 100 |
-
"diagonal": 0
|
| 101 |
-
},
|
| 102 |
-
"ocr": {
|
| 103 |
-
"success": true,
|
| 104 |
-
"time_s": 0.596,
|
| 105 |
-
"element_count": 4,
|
| 106 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 107 |
-
"classified": {
|
| 108 |
-
"dimensions": [
|
| 109 |
-
"3640mm"
|
| 110 |
-
],
|
| 111 |
-
"room_names": [
|
| 112 |
-
"Bedroom",
|
| 113 |
-
"Living Room"
|
| 114 |
-
],
|
| 115 |
-
"numbers": [],
|
| 116 |
-
"labels": [
|
| 117 |
-
"1:100"
|
| 118 |
-
]
|
| 119 |
-
}
|
| 120 |
-
},
|
| 121 |
-
"svg": {
|
| 122 |
-
"success": true,
|
| 123 |
-
"time_s": 0.003,
|
| 124 |
-
"svg_size_bytes": 2796
|
| 125 |
-
}
|
| 126 |
-
},
|
| 127 |
-
"errors": [],
|
| 128 |
-
"total_time_s": 1.231,
|
| 129 |
-
"accuracy": {
|
| 130 |
-
"ocr_cer": 0.3125,
|
| 131 |
-
"ocr_wer": 0.2,
|
| 132 |
-
"ocr_token_recall": 1.0,
|
| 133 |
-
"ocr_token_precision": 1.0,
|
| 134 |
-
"ocr_token_f1": 1.0,
|
| 135 |
-
"room_name_recall": 1.0,
|
| 136 |
-
"line_count_accuracy": 1.0
|
| 137 |
-
}
|
| 138 |
-
}
|
| 139 |
-
],
|
| 140 |
-
"aggregate": {
|
| 141 |
-
"avg_ocr_cer": 0.6562,
|
| 142 |
-
"min_ocr_cer": 0.3125,
|
| 143 |
-
"max_ocr_cer": 1.0,
|
| 144 |
-
"avg_ocr_wer": 0.4654,
|
| 145 |
-
"min_ocr_wer": 0.2,
|
| 146 |
-
"max_ocr_wer": 0.7308,
|
| 147 |
-
"avg_ocr_token_recall": 0.9231,
|
| 148 |
-
"min_ocr_token_recall": 0.8462,
|
| 149 |
-
"max_ocr_token_recall": 1.0,
|
| 150 |
-
"avg_ocr_token_precision": 0.8334,
|
| 151 |
-
"min_ocr_token_precision": 0.6667,
|
| 152 |
-
"max_ocr_token_precision": 1.0,
|
| 153 |
-
"avg_ocr_token_f1": 0.8729,
|
| 154 |
-
"min_ocr_token_f1": 0.7458,
|
| 155 |
-
"max_ocr_token_f1": 1.0,
|
| 156 |
-
"avg_room_name_recall": 1.0,
|
| 157 |
-
"min_room_name_recall": 1.0,
|
| 158 |
-
"max_room_name_recall": 1.0,
|
| 159 |
-
"avg_line_count_accuracy": 1.0,
|
| 160 |
-
"min_line_count_accuracy": 1.0,
|
| 161 |
-
"max_line_count_accuracy": 1.0
|
| 162 |
-
}
|
| 163 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_163623.json
DELETED
|
@@ -1,163 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T16:36:26.607603",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_163623.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.684
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.027,
|
| 25 |
-
"lines_detected": 27,
|
| 26 |
-
"horizontal": 8,
|
| 27 |
-
"vertical": 9,
|
| 28 |
-
"diagonal": 10
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.985,
|
| 33 |
-
"element_count": 18,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6.400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [
|
| 52 |
-
"7",
|
| 53 |
-
"1"
|
| 54 |
-
],
|
| 55 |
-
"labels": [
|
| 56 |
-
"{",
|
| 57 |
-
"\"SCALE",
|
| 58 |
-
"FLOOR PLAN",
|
| 59 |
-
"LEVEL 1",
|
| 60 |
-
"a"
|
| 61 |
-
]
|
| 62 |
-
}
|
| 63 |
-
},
|
| 64 |
-
"svg": {
|
| 65 |
-
"success": true,
|
| 66 |
-
"time_s": 0.002,
|
| 67 |
-
"svg_size_bytes": 4265
|
| 68 |
-
}
|
| 69 |
-
},
|
| 70 |
-
"errors": [],
|
| 71 |
-
"total_time_s": 1.698,
|
| 72 |
-
"accuracy": {
|
| 73 |
-
"ocr_cer": 1.0,
|
| 74 |
-
"ocr_wer": 0.7308,
|
| 75 |
-
"ocr_token_recall": 0.8462,
|
| 76 |
-
"ocr_token_precision": 0.6667,
|
| 77 |
-
"ocr_token_f1": 0.7458,
|
| 78 |
-
"room_name_recall": 1.0,
|
| 79 |
-
"line_count_accuracy": 1.0
|
| 80 |
-
}
|
| 81 |
-
},
|
| 82 |
-
{
|
| 83 |
-
"filename": "synthetic_floor_plan.png",
|
| 84 |
-
"size_bytes": 12406,
|
| 85 |
-
"dimensions": {
|
| 86 |
-
"width": 1200,
|
| 87 |
-
"height": 900
|
| 88 |
-
},
|
| 89 |
-
"stages": {
|
| 90 |
-
"enhancement": {
|
| 91 |
-
"success": true,
|
| 92 |
-
"time_s": 0.57
|
| 93 |
-
},
|
| 94 |
-
"line_extraction": {
|
| 95 |
-
"success": true,
|
| 96 |
-
"time_s": 0.024,
|
| 97 |
-
"lines_detected": 6,
|
| 98 |
-
"horizontal": 3,
|
| 99 |
-
"vertical": 3,
|
| 100 |
-
"diagonal": 0
|
| 101 |
-
},
|
| 102 |
-
"ocr": {
|
| 103 |
-
"success": true,
|
| 104 |
-
"time_s": 0.582,
|
| 105 |
-
"element_count": 4,
|
| 106 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 107 |
-
"classified": {
|
| 108 |
-
"dimensions": [
|
| 109 |
-
"3640mm"
|
| 110 |
-
],
|
| 111 |
-
"room_names": [
|
| 112 |
-
"Bedroom",
|
| 113 |
-
"Living Room"
|
| 114 |
-
],
|
| 115 |
-
"numbers": [],
|
| 116 |
-
"labels": [
|
| 117 |
-
"1:100"
|
| 118 |
-
]
|
| 119 |
-
}
|
| 120 |
-
},
|
| 121 |
-
"svg": {
|
| 122 |
-
"success": true,
|
| 123 |
-
"time_s": 0.001,
|
| 124 |
-
"svg_size_bytes": 1202
|
| 125 |
-
}
|
| 126 |
-
},
|
| 127 |
-
"errors": [],
|
| 128 |
-
"total_time_s": 1.177,
|
| 129 |
-
"accuracy": {
|
| 130 |
-
"ocr_cer": 0.3125,
|
| 131 |
-
"ocr_wer": 0.2,
|
| 132 |
-
"ocr_token_recall": 1.0,
|
| 133 |
-
"ocr_token_precision": 1.0,
|
| 134 |
-
"ocr_token_f1": 1.0,
|
| 135 |
-
"room_name_recall": 1.0,
|
| 136 |
-
"line_count_accuracy": 1.0
|
| 137 |
-
}
|
| 138 |
-
}
|
| 139 |
-
],
|
| 140 |
-
"aggregate": {
|
| 141 |
-
"avg_ocr_cer": 0.6562,
|
| 142 |
-
"min_ocr_cer": 0.3125,
|
| 143 |
-
"max_ocr_cer": 1.0,
|
| 144 |
-
"avg_ocr_wer": 0.4654,
|
| 145 |
-
"min_ocr_wer": 0.2,
|
| 146 |
-
"max_ocr_wer": 0.7308,
|
| 147 |
-
"avg_ocr_token_recall": 0.9231,
|
| 148 |
-
"min_ocr_token_recall": 0.8462,
|
| 149 |
-
"max_ocr_token_recall": 1.0,
|
| 150 |
-
"avg_ocr_token_precision": 0.8334,
|
| 151 |
-
"min_ocr_token_precision": 0.6667,
|
| 152 |
-
"max_ocr_token_precision": 1.0,
|
| 153 |
-
"avg_ocr_token_f1": 0.8729,
|
| 154 |
-
"min_ocr_token_f1": 0.7458,
|
| 155 |
-
"max_ocr_token_f1": 1.0,
|
| 156 |
-
"avg_room_name_recall": 1.0,
|
| 157 |
-
"min_room_name_recall": 1.0,
|
| 158 |
-
"max_room_name_recall": 1.0,
|
| 159 |
-
"avg_line_count_accuracy": 1.0,
|
| 160 |
-
"min_line_count_accuracy": 1.0,
|
| 161 |
-
"max_line_count_accuracy": 1.0
|
| 162 |
-
}
|
| 163 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_163851.json
DELETED
|
@@ -1,158 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T16:38:54.291474",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_163851.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.637
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.026,
|
| 25 |
-
"lines_detected": 27,
|
| 26 |
-
"horizontal": 8,
|
| 27 |
-
"vertical": 9,
|
| 28 |
-
"diagonal": 10
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.849,
|
| 33 |
-
"element_count": 14,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6.400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [],
|
| 52 |
-
"labels": [
|
| 53 |
-
"\"SCALE",
|
| 54 |
-
"FLOOR PLAN",
|
| 55 |
-
"LEVEL 1"
|
| 56 |
-
]
|
| 57 |
-
}
|
| 58 |
-
},
|
| 59 |
-
"svg": {
|
| 60 |
-
"success": true,
|
| 61 |
-
"time_s": 0.001,
|
| 62 |
-
"svg_size_bytes": 3900
|
| 63 |
-
}
|
| 64 |
-
},
|
| 65 |
-
"errors": [],
|
| 66 |
-
"total_time_s": 1.513,
|
| 67 |
-
"accuracy": {
|
| 68 |
-
"ocr_cer": 1.0,
|
| 69 |
-
"ocr_wer": 0.7308,
|
| 70 |
-
"ocr_token_recall": 0.8462,
|
| 71 |
-
"ocr_token_precision": 0.8571,
|
| 72 |
-
"ocr_token_f1": 0.8516,
|
| 73 |
-
"room_name_recall": 1.0,
|
| 74 |
-
"line_count_accuracy": 1.0
|
| 75 |
-
}
|
| 76 |
-
},
|
| 77 |
-
{
|
| 78 |
-
"filename": "synthetic_floor_plan.png",
|
| 79 |
-
"size_bytes": 12406,
|
| 80 |
-
"dimensions": {
|
| 81 |
-
"width": 1200,
|
| 82 |
-
"height": 900
|
| 83 |
-
},
|
| 84 |
-
"stages": {
|
| 85 |
-
"enhancement": {
|
| 86 |
-
"success": true,
|
| 87 |
-
"time_s": 0.564
|
| 88 |
-
},
|
| 89 |
-
"line_extraction": {
|
| 90 |
-
"success": true,
|
| 91 |
-
"time_s": 0.023,
|
| 92 |
-
"lines_detected": 6,
|
| 93 |
-
"horizontal": 3,
|
| 94 |
-
"vertical": 3,
|
| 95 |
-
"diagonal": 0
|
| 96 |
-
},
|
| 97 |
-
"ocr": {
|
| 98 |
-
"success": true,
|
| 99 |
-
"time_s": 0.585,
|
| 100 |
-
"element_count": 4,
|
| 101 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 102 |
-
"classified": {
|
| 103 |
-
"dimensions": [
|
| 104 |
-
"3640mm"
|
| 105 |
-
],
|
| 106 |
-
"room_names": [
|
| 107 |
-
"Bedroom",
|
| 108 |
-
"Living Room"
|
| 109 |
-
],
|
| 110 |
-
"numbers": [],
|
| 111 |
-
"labels": [
|
| 112 |
-
"1:100"
|
| 113 |
-
]
|
| 114 |
-
}
|
| 115 |
-
},
|
| 116 |
-
"svg": {
|
| 117 |
-
"success": true,
|
| 118 |
-
"time_s": 0.001,
|
| 119 |
-
"svg_size_bytes": 1202
|
| 120 |
-
}
|
| 121 |
-
},
|
| 122 |
-
"errors": [],
|
| 123 |
-
"total_time_s": 1.173,
|
| 124 |
-
"accuracy": {
|
| 125 |
-
"ocr_cer": 0.3125,
|
| 126 |
-
"ocr_wer": 0.2,
|
| 127 |
-
"ocr_token_recall": 1.0,
|
| 128 |
-
"ocr_token_precision": 1.0,
|
| 129 |
-
"ocr_token_f1": 1.0,
|
| 130 |
-
"room_name_recall": 1.0,
|
| 131 |
-
"line_count_accuracy": 1.0
|
| 132 |
-
}
|
| 133 |
-
}
|
| 134 |
-
],
|
| 135 |
-
"aggregate": {
|
| 136 |
-
"avg_ocr_cer": 0.6562,
|
| 137 |
-
"min_ocr_cer": 0.3125,
|
| 138 |
-
"max_ocr_cer": 1.0,
|
| 139 |
-
"avg_ocr_wer": 0.4654,
|
| 140 |
-
"min_ocr_wer": 0.2,
|
| 141 |
-
"max_ocr_wer": 0.7308,
|
| 142 |
-
"avg_ocr_token_recall": 0.9231,
|
| 143 |
-
"min_ocr_token_recall": 0.8462,
|
| 144 |
-
"max_ocr_token_recall": 1.0,
|
| 145 |
-
"avg_ocr_token_precision": 0.9285,
|
| 146 |
-
"min_ocr_token_precision": 0.8571,
|
| 147 |
-
"max_ocr_token_precision": 1.0,
|
| 148 |
-
"avg_ocr_token_f1": 0.9258,
|
| 149 |
-
"min_ocr_token_f1": 0.8516,
|
| 150 |
-
"max_ocr_token_f1": 1.0,
|
| 151 |
-
"avg_room_name_recall": 1.0,
|
| 152 |
-
"min_room_name_recall": 1.0,
|
| 153 |
-
"max_room_name_recall": 1.0,
|
| 154 |
-
"avg_line_count_accuracy": 1.0,
|
| 155 |
-
"min_line_count_accuracy": 1.0,
|
| 156 |
-
"max_line_count_accuracy": 1.0
|
| 157 |
-
}
|
| 158 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_164015.json
DELETED
|
@@ -1,158 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T16:40:18.370959",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_164015.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.642
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.026,
|
| 25 |
-
"lines_detected": 27,
|
| 26 |
-
"horizontal": 8,
|
| 27 |
-
"vertical": 9,
|
| 28 |
-
"diagonal": 10
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.852,
|
| 33 |
-
"element_count": 14,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6,400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [],
|
| 52 |
-
"labels": [
|
| 53 |
-
"\"SCALE",
|
| 54 |
-
"FLOOR PLAN",
|
| 55 |
-
"LEVEL 1"
|
| 56 |
-
]
|
| 57 |
-
}
|
| 58 |
-
},
|
| 59 |
-
"svg": {
|
| 60 |
-
"success": true,
|
| 61 |
-
"time_s": 0.001,
|
| 62 |
-
"svg_size_bytes": 3900
|
| 63 |
-
}
|
| 64 |
-
},
|
| 65 |
-
"errors": [],
|
| 66 |
-
"total_time_s": 1.521,
|
| 67 |
-
"accuracy": {
|
| 68 |
-
"ocr_cer": 1.0,
|
| 69 |
-
"ocr_wer": 0.7308,
|
| 70 |
-
"ocr_token_recall": 0.8462,
|
| 71 |
-
"ocr_token_precision": 0.8571,
|
| 72 |
-
"ocr_token_f1": 0.8516,
|
| 73 |
-
"room_name_recall": 1.0,
|
| 74 |
-
"line_count_accuracy": 1.0
|
| 75 |
-
}
|
| 76 |
-
},
|
| 77 |
-
{
|
| 78 |
-
"filename": "synthetic_floor_plan.png",
|
| 79 |
-
"size_bytes": 12406,
|
| 80 |
-
"dimensions": {
|
| 81 |
-
"width": 1200,
|
| 82 |
-
"height": 900
|
| 83 |
-
},
|
| 84 |
-
"stages": {
|
| 85 |
-
"enhancement": {
|
| 86 |
-
"success": true,
|
| 87 |
-
"time_s": 0.56
|
| 88 |
-
},
|
| 89 |
-
"line_extraction": {
|
| 90 |
-
"success": true,
|
| 91 |
-
"time_s": 0.024,
|
| 92 |
-
"lines_detected": 6,
|
| 93 |
-
"horizontal": 3,
|
| 94 |
-
"vertical": 3,
|
| 95 |
-
"diagonal": 0
|
| 96 |
-
},
|
| 97 |
-
"ocr": {
|
| 98 |
-
"success": true,
|
| 99 |
-
"time_s": 0.558,
|
| 100 |
-
"element_count": 4,
|
| 101 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 102 |
-
"classified": {
|
| 103 |
-
"dimensions": [
|
| 104 |
-
"3640mm"
|
| 105 |
-
],
|
| 106 |
-
"room_names": [
|
| 107 |
-
"Bedroom",
|
| 108 |
-
"Living Room"
|
| 109 |
-
],
|
| 110 |
-
"numbers": [],
|
| 111 |
-
"labels": [
|
| 112 |
-
"1:100"
|
| 113 |
-
]
|
| 114 |
-
}
|
| 115 |
-
},
|
| 116 |
-
"svg": {
|
| 117 |
-
"success": true,
|
| 118 |
-
"time_s": 0.001,
|
| 119 |
-
"svg_size_bytes": 1202
|
| 120 |
-
}
|
| 121 |
-
},
|
| 122 |
-
"errors": [],
|
| 123 |
-
"total_time_s": 1.143,
|
| 124 |
-
"accuracy": {
|
| 125 |
-
"ocr_cer": 0.3125,
|
| 126 |
-
"ocr_wer": 0.2,
|
| 127 |
-
"ocr_token_recall": 1.0,
|
| 128 |
-
"ocr_token_precision": 1.0,
|
| 129 |
-
"ocr_token_f1": 1.0,
|
| 130 |
-
"room_name_recall": 1.0,
|
| 131 |
-
"line_count_accuracy": 1.0
|
| 132 |
-
}
|
| 133 |
-
}
|
| 134 |
-
],
|
| 135 |
-
"aggregate": {
|
| 136 |
-
"avg_ocr_cer": 0.6562,
|
| 137 |
-
"min_ocr_cer": 0.3125,
|
| 138 |
-
"max_ocr_cer": 1.0,
|
| 139 |
-
"avg_ocr_wer": 0.4654,
|
| 140 |
-
"min_ocr_wer": 0.2,
|
| 141 |
-
"max_ocr_wer": 0.7308,
|
| 142 |
-
"avg_ocr_token_recall": 0.9231,
|
| 143 |
-
"min_ocr_token_recall": 0.8462,
|
| 144 |
-
"max_ocr_token_recall": 1.0,
|
| 145 |
-
"avg_ocr_token_precision": 0.9285,
|
| 146 |
-
"min_ocr_token_precision": 0.8571,
|
| 147 |
-
"max_ocr_token_precision": 1.0,
|
| 148 |
-
"avg_ocr_token_f1": 0.9258,
|
| 149 |
-
"min_ocr_token_f1": 0.8516,
|
| 150 |
-
"max_ocr_token_f1": 1.0,
|
| 151 |
-
"avg_room_name_recall": 1.0,
|
| 152 |
-
"min_room_name_recall": 1.0,
|
| 153 |
-
"max_room_name_recall": 1.0,
|
| 154 |
-
"avg_line_count_accuracy": 1.0,
|
| 155 |
-
"min_line_count_accuracy": 1.0,
|
| 156 |
-
"max_line_count_accuracy": 1.0
|
| 157 |
-
}
|
| 158 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_164018.json
DELETED
|
@@ -1,158 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T16:40:21.307646",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_164018.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.631
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.026,
|
| 25 |
-
"lines_detected": 27,
|
| 26 |
-
"horizontal": 8,
|
| 27 |
-
"vertical": 9,
|
| 28 |
-
"diagonal": 10
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.879,
|
| 33 |
-
"element_count": 14,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6,400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [],
|
| 52 |
-
"labels": [
|
| 53 |
-
"\"SCALE",
|
| 54 |
-
"FLOOR PLAN",
|
| 55 |
-
"LEVEL 1"
|
| 56 |
-
]
|
| 57 |
-
}
|
| 58 |
-
},
|
| 59 |
-
"svg": {
|
| 60 |
-
"success": true,
|
| 61 |
-
"time_s": 0.001,
|
| 62 |
-
"svg_size_bytes": 3900
|
| 63 |
-
}
|
| 64 |
-
},
|
| 65 |
-
"errors": [],
|
| 66 |
-
"total_time_s": 1.537,
|
| 67 |
-
"accuracy": {
|
| 68 |
-
"ocr_cer": 1.0,
|
| 69 |
-
"ocr_wer": 0.7308,
|
| 70 |
-
"ocr_token_recall": 0.8462,
|
| 71 |
-
"ocr_token_precision": 0.8571,
|
| 72 |
-
"ocr_token_f1": 0.8516,
|
| 73 |
-
"room_name_recall": 1.0,
|
| 74 |
-
"line_count_accuracy": 1.0
|
| 75 |
-
}
|
| 76 |
-
},
|
| 77 |
-
{
|
| 78 |
-
"filename": "synthetic_floor_plan.png",
|
| 79 |
-
"size_bytes": 12406,
|
| 80 |
-
"dimensions": {
|
| 81 |
-
"width": 1200,
|
| 82 |
-
"height": 900
|
| 83 |
-
},
|
| 84 |
-
"stages": {
|
| 85 |
-
"enhancement": {
|
| 86 |
-
"success": true,
|
| 87 |
-
"time_s": 0.581
|
| 88 |
-
},
|
| 89 |
-
"line_extraction": {
|
| 90 |
-
"success": true,
|
| 91 |
-
"time_s": 0.024,
|
| 92 |
-
"lines_detected": 6,
|
| 93 |
-
"horizontal": 3,
|
| 94 |
-
"vertical": 3,
|
| 95 |
-
"diagonal": 0
|
| 96 |
-
},
|
| 97 |
-
"ocr": {
|
| 98 |
-
"success": true,
|
| 99 |
-
"time_s": 0.553,
|
| 100 |
-
"element_count": 4,
|
| 101 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 102 |
-
"classified": {
|
| 103 |
-
"dimensions": [
|
| 104 |
-
"3640mm"
|
| 105 |
-
],
|
| 106 |
-
"room_names": [
|
| 107 |
-
"Bedroom",
|
| 108 |
-
"Living Room"
|
| 109 |
-
],
|
| 110 |
-
"numbers": [],
|
| 111 |
-
"labels": [
|
| 112 |
-
"1:100"
|
| 113 |
-
]
|
| 114 |
-
}
|
| 115 |
-
},
|
| 116 |
-
"svg": {
|
| 117 |
-
"success": true,
|
| 118 |
-
"time_s": 0.0,
|
| 119 |
-
"svg_size_bytes": 1202
|
| 120 |
-
}
|
| 121 |
-
},
|
| 122 |
-
"errors": [],
|
| 123 |
-
"total_time_s": 1.158,
|
| 124 |
-
"accuracy": {
|
| 125 |
-
"ocr_cer": 0.3125,
|
| 126 |
-
"ocr_wer": 0.2,
|
| 127 |
-
"ocr_token_recall": 1.0,
|
| 128 |
-
"ocr_token_precision": 1.0,
|
| 129 |
-
"ocr_token_f1": 1.0,
|
| 130 |
-
"room_name_recall": 1.0,
|
| 131 |
-
"line_count_accuracy": 1.0
|
| 132 |
-
}
|
| 133 |
-
}
|
| 134 |
-
],
|
| 135 |
-
"aggregate": {
|
| 136 |
-
"avg_ocr_cer": 0.6562,
|
| 137 |
-
"min_ocr_cer": 0.3125,
|
| 138 |
-
"max_ocr_cer": 1.0,
|
| 139 |
-
"avg_ocr_wer": 0.4654,
|
| 140 |
-
"min_ocr_wer": 0.2,
|
| 141 |
-
"max_ocr_wer": 0.7308,
|
| 142 |
-
"avg_ocr_token_recall": 0.9231,
|
| 143 |
-
"min_ocr_token_recall": 0.8462,
|
| 144 |
-
"max_ocr_token_recall": 1.0,
|
| 145 |
-
"avg_ocr_token_precision": 0.9285,
|
| 146 |
-
"min_ocr_token_precision": 0.8571,
|
| 147 |
-
"max_ocr_token_precision": 1.0,
|
| 148 |
-
"avg_ocr_token_f1": 0.9258,
|
| 149 |
-
"min_ocr_token_f1": 0.8516,
|
| 150 |
-
"max_ocr_token_f1": 1.0,
|
| 151 |
-
"avg_room_name_recall": 1.0,
|
| 152 |
-
"min_room_name_recall": 1.0,
|
| 153 |
-
"max_room_name_recall": 1.0,
|
| 154 |
-
"avg_line_count_accuracy": 1.0,
|
| 155 |
-
"min_line_count_accuracy": 1.0,
|
| 156 |
-
"max_line_count_accuracy": 1.0
|
| 157 |
-
}
|
| 158 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_164213.json
DELETED
|
@@ -1,158 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T16:42:16.087131",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_164213.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.644
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.027,
|
| 25 |
-
"lines_detected": 27,
|
| 26 |
-
"horizontal": 8,
|
| 27 |
-
"vertical": 9,
|
| 28 |
-
"diagonal": 10
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.925,
|
| 33 |
-
"element_count": 14,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6,400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [],
|
| 52 |
-
"labels": [
|
| 53 |
-
"SCALE",
|
| 54 |
-
"FLOOR PLAN",
|
| 55 |
-
"LEVEL 1"
|
| 56 |
-
]
|
| 57 |
-
}
|
| 58 |
-
},
|
| 59 |
-
"svg": {
|
| 60 |
-
"success": true,
|
| 61 |
-
"time_s": 0.001,
|
| 62 |
-
"svg_size_bytes": 3899
|
| 63 |
-
}
|
| 64 |
-
},
|
| 65 |
-
"errors": [],
|
| 66 |
-
"total_time_s": 1.597,
|
| 67 |
-
"accuracy": {
|
| 68 |
-
"ocr_cer": 1.0,
|
| 69 |
-
"ocr_wer": 0.7308,
|
| 70 |
-
"ocr_token_recall": 0.9231,
|
| 71 |
-
"ocr_token_precision": 0.9286,
|
| 72 |
-
"ocr_token_f1": 0.9258,
|
| 73 |
-
"room_name_recall": 1.0,
|
| 74 |
-
"line_count_accuracy": 1.0
|
| 75 |
-
}
|
| 76 |
-
},
|
| 77 |
-
{
|
| 78 |
-
"filename": "synthetic_floor_plan.png",
|
| 79 |
-
"size_bytes": 12406,
|
| 80 |
-
"dimensions": {
|
| 81 |
-
"width": 1200,
|
| 82 |
-
"height": 900
|
| 83 |
-
},
|
| 84 |
-
"stages": {
|
| 85 |
-
"enhancement": {
|
| 86 |
-
"success": true,
|
| 87 |
-
"time_s": 0.58
|
| 88 |
-
},
|
| 89 |
-
"line_extraction": {
|
| 90 |
-
"success": true,
|
| 91 |
-
"time_s": 0.025,
|
| 92 |
-
"lines_detected": 6,
|
| 93 |
-
"horizontal": 3,
|
| 94 |
-
"vertical": 3,
|
| 95 |
-
"diagonal": 0
|
| 96 |
-
},
|
| 97 |
-
"ocr": {
|
| 98 |
-
"success": true,
|
| 99 |
-
"time_s": 0.569,
|
| 100 |
-
"element_count": 4,
|
| 101 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 102 |
-
"classified": {
|
| 103 |
-
"dimensions": [
|
| 104 |
-
"3640mm"
|
| 105 |
-
],
|
| 106 |
-
"room_names": [
|
| 107 |
-
"Bedroom",
|
| 108 |
-
"Living Room"
|
| 109 |
-
],
|
| 110 |
-
"numbers": [],
|
| 111 |
-
"labels": [
|
| 112 |
-
"1:100"
|
| 113 |
-
]
|
| 114 |
-
}
|
| 115 |
-
},
|
| 116 |
-
"svg": {
|
| 117 |
-
"success": true,
|
| 118 |
-
"time_s": 0.001,
|
| 119 |
-
"svg_size_bytes": 1202
|
| 120 |
-
}
|
| 121 |
-
},
|
| 122 |
-
"errors": [],
|
| 123 |
-
"total_time_s": 1.175,
|
| 124 |
-
"accuracy": {
|
| 125 |
-
"ocr_cer": 0.3125,
|
| 126 |
-
"ocr_wer": 0.2,
|
| 127 |
-
"ocr_token_recall": 1.0,
|
| 128 |
-
"ocr_token_precision": 1.0,
|
| 129 |
-
"ocr_token_f1": 1.0,
|
| 130 |
-
"room_name_recall": 1.0,
|
| 131 |
-
"line_count_accuracy": 1.0
|
| 132 |
-
}
|
| 133 |
-
}
|
| 134 |
-
],
|
| 135 |
-
"aggregate": {
|
| 136 |
-
"avg_ocr_cer": 0.6562,
|
| 137 |
-
"min_ocr_cer": 0.3125,
|
| 138 |
-
"max_ocr_cer": 1.0,
|
| 139 |
-
"avg_ocr_wer": 0.4654,
|
| 140 |
-
"min_ocr_wer": 0.2,
|
| 141 |
-
"max_ocr_wer": 0.7308,
|
| 142 |
-
"avg_ocr_token_recall": 0.9616,
|
| 143 |
-
"min_ocr_token_recall": 0.9231,
|
| 144 |
-
"max_ocr_token_recall": 1.0,
|
| 145 |
-
"avg_ocr_token_precision": 0.9643,
|
| 146 |
-
"min_ocr_token_precision": 0.9286,
|
| 147 |
-
"max_ocr_token_precision": 1.0,
|
| 148 |
-
"avg_ocr_token_f1": 0.9629,
|
| 149 |
-
"min_ocr_token_f1": 0.9258,
|
| 150 |
-
"max_ocr_token_f1": 1.0,
|
| 151 |
-
"avg_room_name_recall": 1.0,
|
| 152 |
-
"min_room_name_recall": 1.0,
|
| 153 |
-
"max_room_name_recall": 1.0,
|
| 154 |
-
"avg_line_count_accuracy": 1.0,
|
| 155 |
-
"min_line_count_accuracy": 1.0,
|
| 156 |
-
"max_line_count_accuracy": 1.0
|
| 157 |
-
}
|
| 158 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260707_181600.json
DELETED
|
@@ -1,158 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-07T18:16:02.710676",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "/Users/aaronlee/Downloads/architectural-drawing-poc/backend/benchmark/results/benchmark_20260707_181600.json",
|
| 7 |
-
"pipeline_version": "1.0.0"
|
| 8 |
-
},
|
| 9 |
-
"images": [
|
| 10 |
-
{
|
| 11 |
-
"filename": "floor_plan_level1.jpg",
|
| 12 |
-
"size_bytes": 233218,
|
| 13 |
-
"dimensions": {
|
| 14 |
-
"width": 1400,
|
| 15 |
-
"height": 1000
|
| 16 |
-
},
|
| 17 |
-
"stages": {
|
| 18 |
-
"enhancement": {
|
| 19 |
-
"success": true,
|
| 20 |
-
"time_s": 0.628
|
| 21 |
-
},
|
| 22 |
-
"line_extraction": {
|
| 23 |
-
"success": true,
|
| 24 |
-
"time_s": 0.025,
|
| 25 |
-
"lines_detected": 27,
|
| 26 |
-
"horizontal": 8,
|
| 27 |
-
"vertical": 9,
|
| 28 |
-
"diagonal": 10
|
| 29 |
-
},
|
| 30 |
-
"ocr": {
|
| 31 |
-
"success": true,
|
| 32 |
-
"time_s": 0.861,
|
| 33 |
-
"element_count": 14,
|
| 34 |
-
"full_text": "7 1Z,000mm |\n\nBedroom 1 Kitchen Bathroom\n3,640 x 3,640 2,730 x 4,000 1,820 x 3,000\n\nBedroom 2 Living Room\n3,640 x 3,640 8,190 x 4,000\n\n1 {\n\"SCALE 1:100690mm FLOOR PLAN — LEVEL 1 6.400mm",
|
| 35 |
-
"classified": {
|
| 36 |
-
"dimensions": [
|
| 37 |
-
"3,640 x 3,640",
|
| 38 |
-
"2,730 x 4,000",
|
| 39 |
-
"1,820 x 3,000",
|
| 40 |
-
"3,640 x 3,640",
|
| 41 |
-
"8,190 x 4,000",
|
| 42 |
-
"6,400mm"
|
| 43 |
-
],
|
| 44 |
-
"room_names": [
|
| 45 |
-
"Bedroom 1",
|
| 46 |
-
"Kitchen",
|
| 47 |
-
"Bathroom",
|
| 48 |
-
"Bedroom 2",
|
| 49 |
-
"Living Room"
|
| 50 |
-
],
|
| 51 |
-
"numbers": [],
|
| 52 |
-
"labels": [
|
| 53 |
-
"SCALE",
|
| 54 |
-
"FLOOR PLAN",
|
| 55 |
-
"LEVEL 1"
|
| 56 |
-
]
|
| 57 |
-
}
|
| 58 |
-
},
|
| 59 |
-
"svg": {
|
| 60 |
-
"success": true,
|
| 61 |
-
"time_s": 0.001,
|
| 62 |
-
"svg_size_bytes": 3899
|
| 63 |
-
}
|
| 64 |
-
},
|
| 65 |
-
"errors": [],
|
| 66 |
-
"total_time_s": 1.515,
|
| 67 |
-
"accuracy": {
|
| 68 |
-
"ocr_cer": 1.0,
|
| 69 |
-
"ocr_wer": 0.7308,
|
| 70 |
-
"ocr_token_recall": 0.9231,
|
| 71 |
-
"ocr_token_precision": 0.9286,
|
| 72 |
-
"ocr_token_f1": 0.9258,
|
| 73 |
-
"room_name_recall": 1.0,
|
| 74 |
-
"line_count_accuracy": 1.0
|
| 75 |
-
}
|
| 76 |
-
},
|
| 77 |
-
{
|
| 78 |
-
"filename": "synthetic_floor_plan.png",
|
| 79 |
-
"size_bytes": 12406,
|
| 80 |
-
"dimensions": {
|
| 81 |
-
"width": 1200,
|
| 82 |
-
"height": 900
|
| 83 |
-
},
|
| 84 |
-
"stages": {
|
| 85 |
-
"enhancement": {
|
| 86 |
-
"success": true,
|
| 87 |
-
"time_s": 0.556
|
| 88 |
-
},
|
| 89 |
-
"line_extraction": {
|
| 90 |
-
"success": true,
|
| 91 |
-
"time_s": 0.024,
|
| 92 |
-
"lines_detected": 6,
|
| 93 |
-
"horizontal": 3,
|
| 94 |
-
"vertical": 3,
|
| 95 |
-
"diagonal": 0
|
| 96 |
-
},
|
| 97 |
-
"ocr": {
|
| 98 |
-
"success": true,
|
| 99 |
-
"time_s": 0.55,
|
| 100 |
-
"element_count": 4,
|
| 101 |
-
"full_text": "Bedroom\n\nLiving Room\n\n1:100",
|
| 102 |
-
"classified": {
|
| 103 |
-
"dimensions": [
|
| 104 |
-
"3640mm"
|
| 105 |
-
],
|
| 106 |
-
"room_names": [
|
| 107 |
-
"Bedroom",
|
| 108 |
-
"Living Room"
|
| 109 |
-
],
|
| 110 |
-
"numbers": [],
|
| 111 |
-
"labels": [
|
| 112 |
-
"1:100"
|
| 113 |
-
]
|
| 114 |
-
}
|
| 115 |
-
},
|
| 116 |
-
"svg": {
|
| 117 |
-
"success": true,
|
| 118 |
-
"time_s": 0.0,
|
| 119 |
-
"svg_size_bytes": 1202
|
| 120 |
-
}
|
| 121 |
-
},
|
| 122 |
-
"errors": [],
|
| 123 |
-
"total_time_s": 1.13,
|
| 124 |
-
"accuracy": {
|
| 125 |
-
"ocr_cer": 0.3125,
|
| 126 |
-
"ocr_wer": 0.2,
|
| 127 |
-
"ocr_token_recall": 1.0,
|
| 128 |
-
"ocr_token_precision": 1.0,
|
| 129 |
-
"ocr_token_f1": 1.0,
|
| 130 |
-
"room_name_recall": 1.0,
|
| 131 |
-
"line_count_accuracy": 1.0
|
| 132 |
-
}
|
| 133 |
-
}
|
| 134 |
-
],
|
| 135 |
-
"aggregate": {
|
| 136 |
-
"avg_ocr_cer": 0.6562,
|
| 137 |
-
"min_ocr_cer": 0.3125,
|
| 138 |
-
"max_ocr_cer": 1.0,
|
| 139 |
-
"avg_ocr_wer": 0.4654,
|
| 140 |
-
"min_ocr_wer": 0.2,
|
| 141 |
-
"max_ocr_wer": 0.7308,
|
| 142 |
-
"avg_ocr_token_recall": 0.9616,
|
| 143 |
-
"min_ocr_token_recall": 0.9231,
|
| 144 |
-
"max_ocr_token_recall": 1.0,
|
| 145 |
-
"avg_ocr_token_precision": 0.9643,
|
| 146 |
-
"min_ocr_token_precision": 0.9286,
|
| 147 |
-
"max_ocr_token_precision": 1.0,
|
| 148 |
-
"avg_ocr_token_f1": 0.9629,
|
| 149 |
-
"min_ocr_token_f1": 0.9258,
|
| 150 |
-
"max_ocr_token_f1": 1.0,
|
| 151 |
-
"avg_room_name_recall": 1.0,
|
| 152 |
-
"min_room_name_recall": 1.0,
|
| 153 |
-
"max_room_name_recall": 1.0,
|
| 154 |
-
"avg_line_count_accuracy": 1.0,
|
| 155 |
-
"min_line_count_accuracy": 1.0,
|
| 156 |
-
"max_line_count_accuracy": 1.0
|
| 157 |
-
}
|
| 158 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260708_034005.json
DELETED
|
@@ -1,135 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-08T03:40:13.475146",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "C:\\Users\\drago\\Downloads\\architecture-poc\\backend\\benchmark\\results\\benchmark_20260708_034005.json",
|
| 7 |
-
"pipeline_version": "1.0.0",
|
| 8 |
-
"ocr_model": "paddleocr"
|
| 9 |
-
},
|
| 10 |
-
"images": [
|
| 11 |
-
{
|
| 12 |
-
"filename": "floor_plan_level1.jpg",
|
| 13 |
-
"size_bytes": 233218,
|
| 14 |
-
"dimensions": {
|
| 15 |
-
"width": 1400,
|
| 16 |
-
"height": 1000
|
| 17 |
-
},
|
| 18 |
-
"stages": {
|
| 19 |
-
"enhancement": {
|
| 20 |
-
"success": true,
|
| 21 |
-
"time_s": 1.21
|
| 22 |
-
},
|
| 23 |
-
"line_extraction": {
|
| 24 |
-
"success": true,
|
| 25 |
-
"time_s": 0.069,
|
| 26 |
-
"lines_detected": 27,
|
| 27 |
-
"horizontal": 8,
|
| 28 |
-
"vertical": 9,
|
| 29 |
-
"diagonal": 10
|
| 30 |
-
},
|
| 31 |
-
"ocr": {
|
| 32 |
-
"success": true,
|
| 33 |
-
"time_s": 5.352,
|
| 34 |
-
"element_count": 0,
|
| 35 |
-
"full_text": "",
|
| 36 |
-
"classified": {
|
| 37 |
-
"dimensions": [],
|
| 38 |
-
"room_names": [],
|
| 39 |
-
"numbers": [],
|
| 40 |
-
"labels": []
|
| 41 |
-
}
|
| 42 |
-
},
|
| 43 |
-
"svg": {
|
| 44 |
-
"success": true,
|
| 45 |
-
"time_s": 0.003,
|
| 46 |
-
"svg_size_bytes": 2463
|
| 47 |
-
}
|
| 48 |
-
},
|
| 49 |
-
"errors": [],
|
| 50 |
-
"total_time_s": 6.634,
|
| 51 |
-
"accuracy": {
|
| 52 |
-
"ocr_cer": 1.0,
|
| 53 |
-
"ocr_wer": 1.0,
|
| 54 |
-
"ocr_token_recall": 0.0,
|
| 55 |
-
"ocr_token_precision": 0.0,
|
| 56 |
-
"ocr_token_f1": 0.0,
|
| 57 |
-
"room_name_recall": 0.0,
|
| 58 |
-
"line_count_accuracy": 1.0
|
| 59 |
-
}
|
| 60 |
-
},
|
| 61 |
-
{
|
| 62 |
-
"filename": "synthetic_floor_plan.png",
|
| 63 |
-
"size_bytes": 12406,
|
| 64 |
-
"dimensions": {
|
| 65 |
-
"width": 1200,
|
| 66 |
-
"height": 900
|
| 67 |
-
},
|
| 68 |
-
"stages": {
|
| 69 |
-
"enhancement": {
|
| 70 |
-
"success": true,
|
| 71 |
-
"time_s": 0.913
|
| 72 |
-
},
|
| 73 |
-
"line_extraction": {
|
| 74 |
-
"success": true,
|
| 75 |
-
"time_s": 0.053,
|
| 76 |
-
"lines_detected": 6,
|
| 77 |
-
"horizontal": 3,
|
| 78 |
-
"vertical": 3,
|
| 79 |
-
"diagonal": 0
|
| 80 |
-
},
|
| 81 |
-
"ocr": {
|
| 82 |
-
"success": true,
|
| 83 |
-
"time_s": 0.03,
|
| 84 |
-
"element_count": 0,
|
| 85 |
-
"full_text": "",
|
| 86 |
-
"classified": {
|
| 87 |
-
"dimensions": [],
|
| 88 |
-
"room_names": [],
|
| 89 |
-
"numbers": [],
|
| 90 |
-
"labels": []
|
| 91 |
-
}
|
| 92 |
-
},
|
| 93 |
-
"svg": {
|
| 94 |
-
"success": true,
|
| 95 |
-
"time_s": 0.001,
|
| 96 |
-
"svg_size_bytes": 784
|
| 97 |
-
}
|
| 98 |
-
},
|
| 99 |
-
"errors": [],
|
| 100 |
-
"total_time_s": 0.997,
|
| 101 |
-
"accuracy": {
|
| 102 |
-
"ocr_cer": 1.0,
|
| 103 |
-
"ocr_wer": 1.0,
|
| 104 |
-
"ocr_token_recall": 0.0,
|
| 105 |
-
"ocr_token_precision": 0.0,
|
| 106 |
-
"ocr_token_f1": 0.0,
|
| 107 |
-
"room_name_recall": 0.0,
|
| 108 |
-
"line_count_accuracy": 1.0
|
| 109 |
-
}
|
| 110 |
-
}
|
| 111 |
-
],
|
| 112 |
-
"aggregate": {
|
| 113 |
-
"avg_ocr_cer": 1.0,
|
| 114 |
-
"min_ocr_cer": 1.0,
|
| 115 |
-
"max_ocr_cer": 1.0,
|
| 116 |
-
"avg_ocr_wer": 1.0,
|
| 117 |
-
"min_ocr_wer": 1.0,
|
| 118 |
-
"max_ocr_wer": 1.0,
|
| 119 |
-
"avg_ocr_token_recall": 0.0,
|
| 120 |
-
"min_ocr_token_recall": 0.0,
|
| 121 |
-
"max_ocr_token_recall": 0.0,
|
| 122 |
-
"avg_ocr_token_precision": 0.0,
|
| 123 |
-
"min_ocr_token_precision": 0.0,
|
| 124 |
-
"max_ocr_token_precision": 0.0,
|
| 125 |
-
"avg_ocr_token_f1": 0.0,
|
| 126 |
-
"min_ocr_token_f1": 0.0,
|
| 127 |
-
"max_ocr_token_f1": 0.0,
|
| 128 |
-
"avg_room_name_recall": 0.0,
|
| 129 |
-
"min_room_name_recall": 0.0,
|
| 130 |
-
"max_room_name_recall": 0.0,
|
| 131 |
-
"avg_line_count_accuracy": 1.0,
|
| 132 |
-
"min_line_count_accuracy": 1.0,
|
| 133 |
-
"max_line_count_accuracy": 1.0
|
| 134 |
-
}
|
| 135 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260708_034317.json
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-08T03:43:54.440447",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "C:\\Users\\drago\\Downloads\\architecture-poc\\backend\\benchmark\\results\\benchmark_20260708_034317.json",
|
| 7 |
-
"pipeline_version": "1.0.0",
|
| 8 |
-
"ocr_model": "paddleocr"
|
| 9 |
-
},
|
| 10 |
-
"images": [
|
| 11 |
-
{
|
| 12 |
-
"filename": "floor_plan_level1.jpg",
|
| 13 |
-
"size_bytes": 233218,
|
| 14 |
-
"dimensions": {
|
| 15 |
-
"width": 1400,
|
| 16 |
-
"height": 1000
|
| 17 |
-
},
|
| 18 |
-
"stages": {
|
| 19 |
-
"enhancement": {
|
| 20 |
-
"success": true,
|
| 21 |
-
"time_s": 1.095
|
| 22 |
-
},
|
| 23 |
-
"line_extraction": {
|
| 24 |
-
"success": true,
|
| 25 |
-
"time_s": 0.058,
|
| 26 |
-
"lines_detected": 27,
|
| 27 |
-
"horizontal": 8,
|
| 28 |
-
"vertical": 9,
|
| 29 |
-
"diagonal": 10
|
| 30 |
-
},
|
| 31 |
-
"ocr": {
|
| 32 |
-
"success": true,
|
| 33 |
-
"time_s": 21.326,
|
| 34 |
-
"element_count": 14,
|
| 35 |
-
"full_text": "12,000mm\nBedroom 1\nKitchen\nBathroom\n2,730 � 4,000\n3,640 � 3,640\n1,820 � 3,000\nBedroom 2\nLiving Room\n3,640 � 3,640\n8,190 � 4,000\nSCALE 1:100,600mm\nFLOOR PLAN - LEVEL 1\n6,400mm",
|
| 36 |
-
"classified": {
|
| 37 |
-
"dimensions": [
|
| 38 |
-
"12,000mm",
|
| 39 |
-
"2,730 � 4,000",
|
| 40 |
-
"3,640 � 3,640",
|
| 41 |
-
"1,820 � 3,000",
|
| 42 |
-
"3,640 � 3,640",
|
| 43 |
-
"8,190 � 4,000",
|
| 44 |
-
"6,400mm"
|
| 45 |
-
],
|
| 46 |
-
"room_names": [
|
| 47 |
-
"Bedroom 1",
|
| 48 |
-
"Kitchen",
|
| 49 |
-
"Bathroom",
|
| 50 |
-
"Bedroom 2",
|
| 51 |
-
"Living Room"
|
| 52 |
-
],
|
| 53 |
-
"numbers": [],
|
| 54 |
-
"labels": [
|
| 55 |
-
"SCALE 1:100,600mm",
|
| 56 |
-
"FLOOR PLAN - LEVEL 1"
|
| 57 |
-
]
|
| 58 |
-
}
|
| 59 |
-
},
|
| 60 |
-
"svg": {
|
| 61 |
-
"success": true,
|
| 62 |
-
"time_s": 0.003,
|
| 63 |
-
"svg_size_bytes": 3925
|
| 64 |
-
}
|
| 65 |
-
},
|
| 66 |
-
"errors": [],
|
| 67 |
-
"total_time_s": 22.482,
|
| 68 |
-
"accuracy": {
|
| 69 |
-
"ocr_cer": 0.6,
|
| 70 |
-
"ocr_wer": 0.8077,
|
| 71 |
-
"ocr_token_recall": 0.3846,
|
| 72 |
-
"ocr_token_precision": 0.3571,
|
| 73 |
-
"ocr_token_f1": 0.3704,
|
| 74 |
-
"room_name_recall": 1.0,
|
| 75 |
-
"line_count_accuracy": 1.0
|
| 76 |
-
}
|
| 77 |
-
},
|
| 78 |
-
{
|
| 79 |
-
"filename": "synthetic_floor_plan.png",
|
| 80 |
-
"size_bytes": 12406,
|
| 81 |
-
"dimensions": {
|
| 82 |
-
"width": 1200,
|
| 83 |
-
"height": 900
|
| 84 |
-
},
|
| 85 |
-
"stages": {
|
| 86 |
-
"enhancement": {
|
| 87 |
-
"success": true,
|
| 88 |
-
"time_s": 1.049
|
| 89 |
-
},
|
| 90 |
-
"line_extraction": {
|
| 91 |
-
"success": true,
|
| 92 |
-
"time_s": 0.082,
|
| 93 |
-
"lines_detected": 6,
|
| 94 |
-
"horizontal": 3,
|
| 95 |
-
"vertical": 3,
|
| 96 |
-
"diagonal": 0
|
| 97 |
-
},
|
| 98 |
-
"ocr": {
|
| 99 |
-
"success": true,
|
| 100 |
-
"time_s": 12.898,
|
| 101 |
-
"element_count": 4,
|
| 102 |
-
"full_text": "Bedroom\n3640mm\nLiving Room\n1:100",
|
| 103 |
-
"classified": {
|
| 104 |
-
"dimensions": [
|
| 105 |
-
"3640mm"
|
| 106 |
-
],
|
| 107 |
-
"room_names": [
|
| 108 |
-
"Bedroom",
|
| 109 |
-
"Living Room"
|
| 110 |
-
],
|
| 111 |
-
"numbers": [],
|
| 112 |
-
"labels": [
|
| 113 |
-
"1:100"
|
| 114 |
-
]
|
| 115 |
-
}
|
| 116 |
-
},
|
| 117 |
-
"svg": {
|
| 118 |
-
"success": true,
|
| 119 |
-
"time_s": 0.001,
|
| 120 |
-
"svg_size_bytes": 1202
|
| 121 |
-
}
|
| 122 |
-
},
|
| 123 |
-
"errors": [],
|
| 124 |
-
"total_time_s": 14.03,
|
| 125 |
-
"accuracy": {
|
| 126 |
-
"ocr_cer": 0.5,
|
| 127 |
-
"ocr_wer": 0.4,
|
| 128 |
-
"ocr_token_recall": 1.0,
|
| 129 |
-
"ocr_token_precision": 1.0,
|
| 130 |
-
"ocr_token_f1": 1.0,
|
| 131 |
-
"room_name_recall": 1.0,
|
| 132 |
-
"line_count_accuracy": 1.0
|
| 133 |
-
}
|
| 134 |
-
}
|
| 135 |
-
],
|
| 136 |
-
"aggregate": {
|
| 137 |
-
"avg_ocr_cer": 0.55,
|
| 138 |
-
"min_ocr_cer": 0.5,
|
| 139 |
-
"max_ocr_cer": 0.6,
|
| 140 |
-
"avg_ocr_wer": 0.6038,
|
| 141 |
-
"min_ocr_wer": 0.4,
|
| 142 |
-
"max_ocr_wer": 0.8077,
|
| 143 |
-
"avg_ocr_token_recall": 0.6923,
|
| 144 |
-
"min_ocr_token_recall": 0.3846,
|
| 145 |
-
"max_ocr_token_recall": 1.0,
|
| 146 |
-
"avg_ocr_token_precision": 0.6785,
|
| 147 |
-
"min_ocr_token_precision": 0.3571,
|
| 148 |
-
"max_ocr_token_precision": 1.0,
|
| 149 |
-
"avg_ocr_token_f1": 0.6852,
|
| 150 |
-
"min_ocr_token_f1": 0.3704,
|
| 151 |
-
"max_ocr_token_f1": 1.0,
|
| 152 |
-
"avg_room_name_recall": 1.0,
|
| 153 |
-
"min_room_name_recall": 1.0,
|
| 154 |
-
"max_room_name_recall": 1.0,
|
| 155 |
-
"avg_line_count_accuracy": 1.0,
|
| 156 |
-
"min_line_count_accuracy": 1.0,
|
| 157 |
-
"max_line_count_accuracy": 1.0
|
| 158 |
-
}
|
| 159 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260708_034854.json
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-08T03:49:28.572783",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "C:\\Users\\drago\\Downloads\\architecture-poc\\backend\\benchmark\\results\\benchmark_20260708_034854.json",
|
| 7 |
-
"pipeline_version": "1.0.0",
|
| 8 |
-
"ocr_model": "paddleocr"
|
| 9 |
-
},
|
| 10 |
-
"images": [
|
| 11 |
-
{
|
| 12 |
-
"filename": "floor_plan_level1.jpg",
|
| 13 |
-
"size_bytes": 233218,
|
| 14 |
-
"dimensions": {
|
| 15 |
-
"width": 1400,
|
| 16 |
-
"height": 1000
|
| 17 |
-
},
|
| 18 |
-
"stages": {
|
| 19 |
-
"enhancement": {
|
| 20 |
-
"success": true,
|
| 21 |
-
"time_s": 1.087
|
| 22 |
-
},
|
| 23 |
-
"line_extraction": {
|
| 24 |
-
"success": true,
|
| 25 |
-
"time_s": 0.059,
|
| 26 |
-
"lines_detected": 27,
|
| 27 |
-
"horizontal": 8,
|
| 28 |
-
"vertical": 9,
|
| 29 |
-
"diagonal": 10
|
| 30 |
-
},
|
| 31 |
-
"ocr": {
|
| 32 |
-
"success": true,
|
| 33 |
-
"time_s": 20.128,
|
| 34 |
-
"element_count": 14,
|
| 35 |
-
"full_text": "12,000mm\nBedroom 1\nKitchen\nBathroom\n2,730 × 4,000\n3,640 × 3,640\n1,820 × 3,000\nBedroom 2\nLiving Room\n3,640 × 3,640\n8,190 × 4,000\nSCALE 1:100,600mm\nFLOOR PLAN - LEVEL 1\n6,400mm",
|
| 36 |
-
"classified": {
|
| 37 |
-
"dimensions": [
|
| 38 |
-
"12,000mm",
|
| 39 |
-
"2,730 × 4,000",
|
| 40 |
-
"3,640 × 3,640",
|
| 41 |
-
"1,820 × 3,000",
|
| 42 |
-
"3,640 × 3,640",
|
| 43 |
-
"8,190 × 4,000",
|
| 44 |
-
"6,400mm"
|
| 45 |
-
],
|
| 46 |
-
"room_names": [
|
| 47 |
-
"Bedroom 1",
|
| 48 |
-
"Kitchen",
|
| 49 |
-
"Bathroom",
|
| 50 |
-
"Bedroom 2",
|
| 51 |
-
"Living Room"
|
| 52 |
-
],
|
| 53 |
-
"numbers": [],
|
| 54 |
-
"labels": [
|
| 55 |
-
"SCALE 1:100,600mm",
|
| 56 |
-
"FLOOR PLAN - LEVEL 1"
|
| 57 |
-
]
|
| 58 |
-
}
|
| 59 |
-
},
|
| 60 |
-
"svg": {
|
| 61 |
-
"success": true,
|
| 62 |
-
"time_s": 0.004,
|
| 63 |
-
"svg_size_bytes": 3925
|
| 64 |
-
}
|
| 65 |
-
},
|
| 66 |
-
"errors": [],
|
| 67 |
-
"total_time_s": 21.278,
|
| 68 |
-
"accuracy": {
|
| 69 |
-
"ocr_cer": 0.6,
|
| 70 |
-
"ocr_wer": 0.8077,
|
| 71 |
-
"ocr_token_recall": 0.3846,
|
| 72 |
-
"ocr_token_precision": 0.3571,
|
| 73 |
-
"ocr_token_f1": 0.3704,
|
| 74 |
-
"room_name_recall": 1.0,
|
| 75 |
-
"line_count_accuracy": 1.0
|
| 76 |
-
}
|
| 77 |
-
},
|
| 78 |
-
{
|
| 79 |
-
"filename": "synthetic_floor_plan.png",
|
| 80 |
-
"size_bytes": 12406,
|
| 81 |
-
"dimensions": {
|
| 82 |
-
"width": 1200,
|
| 83 |
-
"height": 900
|
| 84 |
-
},
|
| 85 |
-
"stages": {
|
| 86 |
-
"enhancement": {
|
| 87 |
-
"success": true,
|
| 88 |
-
"time_s": 0.882
|
| 89 |
-
},
|
| 90 |
-
"line_extraction": {
|
| 91 |
-
"success": true,
|
| 92 |
-
"time_s": 0.048,
|
| 93 |
-
"lines_detected": 6,
|
| 94 |
-
"horizontal": 3,
|
| 95 |
-
"vertical": 3,
|
| 96 |
-
"diagonal": 0
|
| 97 |
-
},
|
| 98 |
-
"ocr": {
|
| 99 |
-
"success": true,
|
| 100 |
-
"time_s": 12.043,
|
| 101 |
-
"element_count": 4,
|
| 102 |
-
"full_text": "Bedroom\n3640mm\nLiving Room\n1:100",
|
| 103 |
-
"classified": {
|
| 104 |
-
"dimensions": [
|
| 105 |
-
"3640mm"
|
| 106 |
-
],
|
| 107 |
-
"room_names": [
|
| 108 |
-
"Bedroom",
|
| 109 |
-
"Living Room"
|
| 110 |
-
],
|
| 111 |
-
"numbers": [],
|
| 112 |
-
"labels": [
|
| 113 |
-
"1:100"
|
| 114 |
-
]
|
| 115 |
-
}
|
| 116 |
-
},
|
| 117 |
-
"svg": {
|
| 118 |
-
"success": true,
|
| 119 |
-
"time_s": 0.001,
|
| 120 |
-
"svg_size_bytes": 1202
|
| 121 |
-
}
|
| 122 |
-
},
|
| 123 |
-
"errors": [],
|
| 124 |
-
"total_time_s": 12.974,
|
| 125 |
-
"accuracy": {
|
| 126 |
-
"ocr_cer": 0.5,
|
| 127 |
-
"ocr_wer": 0.4,
|
| 128 |
-
"ocr_token_recall": 1.0,
|
| 129 |
-
"ocr_token_precision": 1.0,
|
| 130 |
-
"ocr_token_f1": 1.0,
|
| 131 |
-
"room_name_recall": 1.0,
|
| 132 |
-
"line_count_accuracy": 1.0
|
| 133 |
-
}
|
| 134 |
-
}
|
| 135 |
-
],
|
| 136 |
-
"aggregate": {
|
| 137 |
-
"avg_ocr_cer": 0.55,
|
| 138 |
-
"min_ocr_cer": 0.5,
|
| 139 |
-
"max_ocr_cer": 0.6,
|
| 140 |
-
"avg_ocr_wer": 0.6038,
|
| 141 |
-
"min_ocr_wer": 0.4,
|
| 142 |
-
"max_ocr_wer": 0.8077,
|
| 143 |
-
"avg_ocr_token_recall": 0.6923,
|
| 144 |
-
"min_ocr_token_recall": 0.3846,
|
| 145 |
-
"max_ocr_token_recall": 1.0,
|
| 146 |
-
"avg_ocr_token_precision": 0.6785,
|
| 147 |
-
"min_ocr_token_precision": 0.3571,
|
| 148 |
-
"max_ocr_token_precision": 1.0,
|
| 149 |
-
"avg_ocr_token_f1": 0.6852,
|
| 150 |
-
"min_ocr_token_f1": 0.3704,
|
| 151 |
-
"max_ocr_token_f1": 1.0,
|
| 152 |
-
"avg_room_name_recall": 1.0,
|
| 153 |
-
"min_room_name_recall": 1.0,
|
| 154 |
-
"max_room_name_recall": 1.0,
|
| 155 |
-
"avg_line_count_accuracy": 1.0,
|
| 156 |
-
"min_line_count_accuracy": 1.0,
|
| 157 |
-
"max_line_count_accuracy": 1.0
|
| 158 |
-
}
|
| 159 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260708_035238.json
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-08T03:53:16.189753",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "C:\\Users\\drago\\Downloads\\architecture-poc\\backend\\benchmark\\results\\benchmark_20260708_035238.json",
|
| 7 |
-
"pipeline_version": "1.0.0",
|
| 8 |
-
"ocr_model": "paddleocr"
|
| 9 |
-
},
|
| 10 |
-
"images": [
|
| 11 |
-
{
|
| 12 |
-
"filename": "floor_plan_level1.jpg",
|
| 13 |
-
"size_bytes": 233218,
|
| 14 |
-
"dimensions": {
|
| 15 |
-
"width": 1400,
|
| 16 |
-
"height": 1000
|
| 17 |
-
},
|
| 18 |
-
"stages": {
|
| 19 |
-
"enhancement": {
|
| 20 |
-
"success": true,
|
| 21 |
-
"time_s": 1.163
|
| 22 |
-
},
|
| 23 |
-
"line_extraction": {
|
| 24 |
-
"success": true,
|
| 25 |
-
"time_s": 0.063,
|
| 26 |
-
"lines_detected": 27,
|
| 27 |
-
"horizontal": 8,
|
| 28 |
-
"vertical": 9,
|
| 29 |
-
"diagonal": 10
|
| 30 |
-
},
|
| 31 |
-
"ocr": {
|
| 32 |
-
"success": true,
|
| 33 |
-
"time_s": 22.366,
|
| 34 |
-
"element_count": 14,
|
| 35 |
-
"full_text": "12,000mm\nBedroom 1\nKitchen\nBathroom\n2,730 × 4,000\n3,640 × 3,640\n1,820 × 3,000\nBedroom 2\nLiving Room\n3,640 × 3,640\n8,190 × 4,000\nSCALE 1:100,600mm\nFLOOR PLAN - LEVEL 1\n6,400mm",
|
| 36 |
-
"classified": {
|
| 37 |
-
"dimensions": [
|
| 38 |
-
"12,000mm",
|
| 39 |
-
"2,730 × 4,000",
|
| 40 |
-
"3,640 × 3,640",
|
| 41 |
-
"1,820 × 3,000",
|
| 42 |
-
"3,640 × 3,640",
|
| 43 |
-
"8,190 × 4,000",
|
| 44 |
-
"6,400mm"
|
| 45 |
-
],
|
| 46 |
-
"room_names": [
|
| 47 |
-
"Bedroom 1",
|
| 48 |
-
"Kitchen",
|
| 49 |
-
"Bathroom",
|
| 50 |
-
"Bedroom 2",
|
| 51 |
-
"Living Room"
|
| 52 |
-
],
|
| 53 |
-
"numbers": [],
|
| 54 |
-
"labels": [
|
| 55 |
-
"SCALE 1:100,600mm",
|
| 56 |
-
"FLOOR PLAN - LEVEL 1"
|
| 57 |
-
]
|
| 58 |
-
}
|
| 59 |
-
},
|
| 60 |
-
"svg": {
|
| 61 |
-
"success": true,
|
| 62 |
-
"time_s": 0.003,
|
| 63 |
-
"svg_size_bytes": 3925
|
| 64 |
-
}
|
| 65 |
-
},
|
| 66 |
-
"errors": [],
|
| 67 |
-
"total_time_s": 23.595,
|
| 68 |
-
"accuracy": {
|
| 69 |
-
"ocr_cer": 0.5704,
|
| 70 |
-
"ocr_wer": 0.6538,
|
| 71 |
-
"ocr_token_recall": 0.6923,
|
| 72 |
-
"ocr_token_precision": 0.7143,
|
| 73 |
-
"ocr_token_f1": 0.7031,
|
| 74 |
-
"room_name_recall": 1.0,
|
| 75 |
-
"line_count_accuracy": 1.0
|
| 76 |
-
}
|
| 77 |
-
},
|
| 78 |
-
{
|
| 79 |
-
"filename": "synthetic_floor_plan.png",
|
| 80 |
-
"size_bytes": 12406,
|
| 81 |
-
"dimensions": {
|
| 82 |
-
"width": 1200,
|
| 83 |
-
"height": 900
|
| 84 |
-
},
|
| 85 |
-
"stages": {
|
| 86 |
-
"enhancement": {
|
| 87 |
-
"success": true,
|
| 88 |
-
"time_s": 0.982
|
| 89 |
-
},
|
| 90 |
-
"line_extraction": {
|
| 91 |
-
"success": true,
|
| 92 |
-
"time_s": 0.055,
|
| 93 |
-
"lines_detected": 6,
|
| 94 |
-
"horizontal": 3,
|
| 95 |
-
"vertical": 3,
|
| 96 |
-
"diagonal": 0
|
| 97 |
-
},
|
| 98 |
-
"ocr": {
|
| 99 |
-
"success": true,
|
| 100 |
-
"time_s": 13.425,
|
| 101 |
-
"element_count": 4,
|
| 102 |
-
"full_text": "Bedroom\n3640mm\nLiving Room\n1:100",
|
| 103 |
-
"classified": {
|
| 104 |
-
"dimensions": [
|
| 105 |
-
"3640mm"
|
| 106 |
-
],
|
| 107 |
-
"room_names": [
|
| 108 |
-
"Bedroom",
|
| 109 |
-
"Living Room"
|
| 110 |
-
],
|
| 111 |
-
"numbers": [],
|
| 112 |
-
"labels": [
|
| 113 |
-
"1:100"
|
| 114 |
-
]
|
| 115 |
-
}
|
| 116 |
-
},
|
| 117 |
-
"svg": {
|
| 118 |
-
"success": true,
|
| 119 |
-
"time_s": 0.001,
|
| 120 |
-
"svg_size_bytes": 1202
|
| 121 |
-
}
|
| 122 |
-
},
|
| 123 |
-
"errors": [],
|
| 124 |
-
"total_time_s": 14.463,
|
| 125 |
-
"accuracy": {
|
| 126 |
-
"ocr_cer": 0.5,
|
| 127 |
-
"ocr_wer": 0.4,
|
| 128 |
-
"ocr_token_recall": 1.0,
|
| 129 |
-
"ocr_token_precision": 1.0,
|
| 130 |
-
"ocr_token_f1": 1.0,
|
| 131 |
-
"room_name_recall": 1.0,
|
| 132 |
-
"line_count_accuracy": 1.0
|
| 133 |
-
}
|
| 134 |
-
}
|
| 135 |
-
],
|
| 136 |
-
"aggregate": {
|
| 137 |
-
"avg_ocr_cer": 0.5352,
|
| 138 |
-
"min_ocr_cer": 0.5,
|
| 139 |
-
"max_ocr_cer": 0.5704,
|
| 140 |
-
"avg_ocr_wer": 0.5269,
|
| 141 |
-
"min_ocr_wer": 0.4,
|
| 142 |
-
"max_ocr_wer": 0.6538,
|
| 143 |
-
"avg_ocr_token_recall": 0.8461,
|
| 144 |
-
"min_ocr_token_recall": 0.6923,
|
| 145 |
-
"max_ocr_token_recall": 1.0,
|
| 146 |
-
"avg_ocr_token_precision": 0.8572,
|
| 147 |
-
"min_ocr_token_precision": 0.7143,
|
| 148 |
-
"max_ocr_token_precision": 1.0,
|
| 149 |
-
"avg_ocr_token_f1": 0.8516,
|
| 150 |
-
"min_ocr_token_f1": 0.7031,
|
| 151 |
-
"max_ocr_token_f1": 1.0,
|
| 152 |
-
"avg_room_name_recall": 1.0,
|
| 153 |
-
"min_room_name_recall": 1.0,
|
| 154 |
-
"max_room_name_recall": 1.0,
|
| 155 |
-
"avg_line_count_accuracy": 1.0,
|
| 156 |
-
"min_line_count_accuracy": 1.0,
|
| 157 |
-
"max_line_count_accuracy": 1.0
|
| 158 |
-
}
|
| 159 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/results/benchmark_20260708_035329.json
DELETED
|
@@ -1,135 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"metadata": {
|
| 3 |
-
"timestamp": "2026-07-08T03:53:32.005797",
|
| 4 |
-
"image_count": 2,
|
| 5 |
-
"ground_truth": "benchmark/ground_truth.json",
|
| 6 |
-
"output_path": "C:\\Users\\drago\\Downloads\\architecture-poc\\backend\\benchmark\\results\\benchmark_20260708_035329.json",
|
| 7 |
-
"pipeline_version": "1.0.0",
|
| 8 |
-
"ocr_model": "tesseract"
|
| 9 |
-
},
|
| 10 |
-
"images": [
|
| 11 |
-
{
|
| 12 |
-
"filename": "floor_plan_level1.jpg",
|
| 13 |
-
"size_bytes": 233218,
|
| 14 |
-
"dimensions": {
|
| 15 |
-
"width": 1400,
|
| 16 |
-
"height": 1000
|
| 17 |
-
},
|
| 18 |
-
"stages": {
|
| 19 |
-
"enhancement": {
|
| 20 |
-
"success": true,
|
| 21 |
-
"time_s": 1.153
|
| 22 |
-
},
|
| 23 |
-
"line_extraction": {
|
| 24 |
-
"success": true,
|
| 25 |
-
"time_s": 0.062,
|
| 26 |
-
"lines_detected": 27,
|
| 27 |
-
"horizontal": 8,
|
| 28 |
-
"vertical": 9,
|
| 29 |
-
"diagonal": 10
|
| 30 |
-
},
|
| 31 |
-
"ocr": {
|
| 32 |
-
"success": true,
|
| 33 |
-
"time_s": 0.085,
|
| 34 |
-
"element_count": 0,
|
| 35 |
-
"full_text": "",
|
| 36 |
-
"classified": {
|
| 37 |
-
"dimensions": [],
|
| 38 |
-
"room_names": [],
|
| 39 |
-
"numbers": [],
|
| 40 |
-
"labels": []
|
| 41 |
-
}
|
| 42 |
-
},
|
| 43 |
-
"svg": {
|
| 44 |
-
"success": true,
|
| 45 |
-
"time_s": 0.002,
|
| 46 |
-
"svg_size_bytes": 2463
|
| 47 |
-
}
|
| 48 |
-
},
|
| 49 |
-
"errors": [],
|
| 50 |
-
"total_time_s": 1.302,
|
| 51 |
-
"accuracy": {
|
| 52 |
-
"ocr_cer": 1.0,
|
| 53 |
-
"ocr_wer": 1.0,
|
| 54 |
-
"ocr_token_recall": 0.0,
|
| 55 |
-
"ocr_token_precision": 0.0,
|
| 56 |
-
"ocr_token_f1": 0.0,
|
| 57 |
-
"room_name_recall": 0.0,
|
| 58 |
-
"line_count_accuracy": 1.0
|
| 59 |
-
}
|
| 60 |
-
},
|
| 61 |
-
{
|
| 62 |
-
"filename": "synthetic_floor_plan.png",
|
| 63 |
-
"size_bytes": 12406,
|
| 64 |
-
"dimensions": {
|
| 65 |
-
"width": 1200,
|
| 66 |
-
"height": 900
|
| 67 |
-
},
|
| 68 |
-
"stages": {
|
| 69 |
-
"enhancement": {
|
| 70 |
-
"success": true,
|
| 71 |
-
"time_s": 0.987
|
| 72 |
-
},
|
| 73 |
-
"line_extraction": {
|
| 74 |
-
"success": true,
|
| 75 |
-
"time_s": 0.064,
|
| 76 |
-
"lines_detected": 6,
|
| 77 |
-
"horizontal": 3,
|
| 78 |
-
"vertical": 3,
|
| 79 |
-
"diagonal": 0
|
| 80 |
-
},
|
| 81 |
-
"ocr": {
|
| 82 |
-
"success": true,
|
| 83 |
-
"time_s": 0.054,
|
| 84 |
-
"element_count": 0,
|
| 85 |
-
"full_text": "",
|
| 86 |
-
"classified": {
|
| 87 |
-
"dimensions": [],
|
| 88 |
-
"room_names": [],
|
| 89 |
-
"numbers": [],
|
| 90 |
-
"labels": []
|
| 91 |
-
}
|
| 92 |
-
},
|
| 93 |
-
"svg": {
|
| 94 |
-
"success": true,
|
| 95 |
-
"time_s": 0.001,
|
| 96 |
-
"svg_size_bytes": 784
|
| 97 |
-
}
|
| 98 |
-
},
|
| 99 |
-
"errors": [],
|
| 100 |
-
"total_time_s": 1.106,
|
| 101 |
-
"accuracy": {
|
| 102 |
-
"ocr_cer": 1.0,
|
| 103 |
-
"ocr_wer": 1.0,
|
| 104 |
-
"ocr_token_recall": 0.0,
|
| 105 |
-
"ocr_token_precision": 0.0,
|
| 106 |
-
"ocr_token_f1": 0.0,
|
| 107 |
-
"room_name_recall": 0.0,
|
| 108 |
-
"line_count_accuracy": 1.0
|
| 109 |
-
}
|
| 110 |
-
}
|
| 111 |
-
],
|
| 112 |
-
"aggregate": {
|
| 113 |
-
"avg_ocr_cer": 1.0,
|
| 114 |
-
"min_ocr_cer": 1.0,
|
| 115 |
-
"max_ocr_cer": 1.0,
|
| 116 |
-
"avg_ocr_wer": 1.0,
|
| 117 |
-
"min_ocr_wer": 1.0,
|
| 118 |
-
"max_ocr_wer": 1.0,
|
| 119 |
-
"avg_ocr_token_recall": 0.0,
|
| 120 |
-
"min_ocr_token_recall": 0.0,
|
| 121 |
-
"max_ocr_token_recall": 0.0,
|
| 122 |
-
"avg_ocr_token_precision": 0.0,
|
| 123 |
-
"min_ocr_token_precision": 0.0,
|
| 124 |
-
"max_ocr_token_precision": 0.0,
|
| 125 |
-
"avg_ocr_token_f1": 0.0,
|
| 126 |
-
"min_ocr_token_f1": 0.0,
|
| 127 |
-
"max_ocr_token_f1": 0.0,
|
| 128 |
-
"avg_room_name_recall": 0.0,
|
| 129 |
-
"min_room_name_recall": 0.0,
|
| 130 |
-
"max_room_name_recall": 0.0,
|
| 131 |
-
"avg_line_count_accuracy": 1.0,
|
| 132 |
-
"min_line_count_accuracy": 1.0,
|
| 133 |
-
"max_line_count_accuracy": 1.0
|
| 134 |
-
}
|
| 135 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
benchmark/run_benchmark.py
DELETED
|
@@ -1,432 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Architectural Drawing AI — Benchmark Runner
|
| 4 |
-
============================================
|
| 5 |
-
Runs every image in `sample_images/` through the full processing pipeline
|
| 6 |
-
and measures accuracy against optional ground truth annotations.
|
| 7 |
-
|
| 8 |
-
Usage
|
| 9 |
-
-----
|
| 10 |
-
# Basic run (no ground truth — measures timing and detection counts only)
|
| 11 |
-
cd backend
|
| 12 |
-
./venv/bin/python benchmark/run_benchmark.py
|
| 13 |
-
|
| 14 |
-
# With ground truth annotations
|
| 15 |
-
./venv/bin/python benchmark/run_benchmark.py --gt benchmark/ground_truth.json
|
| 16 |
-
|
| 17 |
-
# Save results to a specific file
|
| 18 |
-
./venv/bin/python benchmark/run_benchmark.py --gt benchmark/ground_truth.json \
|
| 19 |
-
--out benchmark/results/run_2026_06_29.json
|
| 20 |
-
|
| 21 |
-
# Verbose output (prints per-image detail)
|
| 22 |
-
./venv/bin/python benchmark/run_benchmark.py -v
|
| 23 |
-
|
| 24 |
-
Output
|
| 25 |
-
------
|
| 26 |
-
Prints a summary table to stdout and writes a full JSON report to
|
| 27 |
-
benchmark/results/benchmark_<timestamp>.json (or --out path).
|
| 28 |
-
|
| 29 |
-
The JSON report is structured for import into the feasibility report and for
|
| 30 |
-
comparison across runs to track accuracy improvements over time.
|
| 31 |
-
"""
|
| 32 |
-
|
| 33 |
-
from __future__ import annotations
|
| 34 |
-
|
| 35 |
-
import argparse
|
| 36 |
-
import json
|
| 37 |
-
import logging
|
| 38 |
-
import os
|
| 39 |
-
import sys
|
| 40 |
-
import time
|
| 41 |
-
from datetime import datetime
|
| 42 |
-
from pathlib import Path
|
| 43 |
-
from typing import Any, Dict, List, Optional
|
| 44 |
-
|
| 45 |
-
import cv2
|
| 46 |
-
import numpy as np
|
| 47 |
-
|
| 48 |
-
# Make sure backend root is on the path regardless of cwd
|
| 49 |
-
ROOT = Path(__file__).resolve().parent.parent
|
| 50 |
-
sys.path.insert(0, str(ROOT))
|
| 51 |
-
|
| 52 |
-
from processors.image_enhancer import ImageEnhancer
|
| 53 |
-
from processors.line_extractor import LineExtractor
|
| 54 |
-
from processors.ocr_processor import OCRProcessor
|
| 55 |
-
from processors.svg_generator import SVGGenerator
|
| 56 |
-
from utils.image_utils import bytes_to_numpy, resize_for_processing
|
| 57 |
-
from benchmark.metrics import (
|
| 58 |
-
character_error_rate,
|
| 59 |
-
word_error_rate,
|
| 60 |
-
token_recall,
|
| 61 |
-
token_precision,
|
| 62 |
-
f1,
|
| 63 |
-
line_count_accuracy,
|
| 64 |
-
)
|
| 65 |
-
|
| 66 |
-
logging.basicConfig(level=logging.WARNING)
|
| 67 |
-
logger = logging.getLogger(__name__)
|
| 68 |
-
|
| 69 |
-
SAMPLE_DIR = ROOT / "benchmark" / "sample_images"
|
| 70 |
-
RESULTS_DIR = ROOT / "benchmark" / "results"
|
| 71 |
-
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 72 |
-
|
| 73 |
-
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".webp"}
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
# ---------------------------------------------------------------------------
|
| 77 |
-
# Processor singletons
|
| 78 |
-
# ---------------------------------------------------------------------------
|
| 79 |
-
_enhancer = ImageEnhancer()
|
| 80 |
-
_extractor = LineExtractor()
|
| 81 |
-
_ocr = OCRProcessor()
|
| 82 |
-
_svg = SVGGenerator()
|
| 83 |
-
_paddleocr = None # lazy — only imported/instantiated if --ocr-model paddleocr
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def _get_ocr_processor(ocr_model: Optional[str]):
|
| 87 |
-
"""Mirrors main.py's _get_ocr_processor: falls back to Tesseract if
|
| 88 |
-
PaddleOCR is requested but unavailable."""
|
| 89 |
-
global _paddleocr
|
| 90 |
-
if ocr_model == "paddleocr":
|
| 91 |
-
if _paddleocr is None:
|
| 92 |
-
from processors.paddleocr_processor import PaddleOCRProcessor
|
| 93 |
-
_paddleocr = PaddleOCRProcessor()
|
| 94 |
-
if _paddleocr.is_available:
|
| 95 |
-
return _paddleocr
|
| 96 |
-
logger.warning(
|
| 97 |
-
"PaddleOCR requested but unavailable (%s) — falling back to Tesseract",
|
| 98 |
-
_paddleocr.unavailable_reason,
|
| 99 |
-
)
|
| 100 |
-
return _ocr
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
# ---------------------------------------------------------------------------
|
| 104 |
-
# Per-image processing
|
| 105 |
-
# ---------------------------------------------------------------------------
|
| 106 |
-
|
| 107 |
-
def process_image(image_path: Path, ocr_model: Optional[str] = None) -> Dict[str, Any]:
|
| 108 |
-
"""
|
| 109 |
-
Run the full pipeline on one image.
|
| 110 |
-
Returns a dict with timings, counts, and all raw results.
|
| 111 |
-
"""
|
| 112 |
-
raw = image_path.read_bytes()
|
| 113 |
-
image = bytes_to_numpy(raw)
|
| 114 |
-
if image is None:
|
| 115 |
-
return {"error": "Cannot decode image", "path": str(image_path)}
|
| 116 |
-
|
| 117 |
-
image = resize_for_processing(image, max_dimension=3000)
|
| 118 |
-
result: Dict[str, Any] = {
|
| 119 |
-
"filename": image_path.name,
|
| 120 |
-
"size_bytes": len(raw),
|
| 121 |
-
"dimensions": {"width": image.shape[1], "height": image.shape[0]},
|
| 122 |
-
"stages": {},
|
| 123 |
-
"errors": [],
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
# Stage 1 — Enhancement
|
| 127 |
-
t0 = time.perf_counter()
|
| 128 |
-
try:
|
| 129 |
-
enhanced = _enhancer.enhance(image)
|
| 130 |
-
result["stages"]["enhancement"] = {
|
| 131 |
-
"success": True,
|
| 132 |
-
"time_s": round(time.perf_counter() - t0, 3),
|
| 133 |
-
}
|
| 134 |
-
except Exception as exc:
|
| 135 |
-
enhanced = image
|
| 136 |
-
result["stages"]["enhancement"] = {"success": False, "error": str(exc)}
|
| 137 |
-
result["errors"].append(f"enhancement: {exc}")
|
| 138 |
-
|
| 139 |
-
# Stage 2 — Line extraction
|
| 140 |
-
t0 = time.perf_counter()
|
| 141 |
-
try:
|
| 142 |
-
_, lines = _extractor.extract(enhanced)
|
| 143 |
-
result["stages"]["line_extraction"] = {
|
| 144 |
-
"success": True,
|
| 145 |
-
"time_s": round(time.perf_counter() - t0, 3),
|
| 146 |
-
"lines_detected": len(lines),
|
| 147 |
-
"horizontal": sum(1 for l in lines if abs(l["angle"]) <= 15 or abs(l["angle"]) >= 165),
|
| 148 |
-
"vertical": sum(1 for l in lines if 75 <= abs(l["angle"]) <= 105),
|
| 149 |
-
"diagonal": sum(1 for l in lines if not (abs(l["angle"]) <= 15 or abs(l["angle"]) >= 165 or 75 <= abs(l["angle"]) <= 105)),
|
| 150 |
-
}
|
| 151 |
-
except Exception as exc:
|
| 152 |
-
lines = []
|
| 153 |
-
result["stages"]["line_extraction"] = {"success": False, "error": str(exc)}
|
| 154 |
-
result["errors"].append(f"line_extraction: {exc}")
|
| 155 |
-
|
| 156 |
-
# Stage 3 — OCR
|
| 157 |
-
t0 = time.perf_counter()
|
| 158 |
-
try:
|
| 159 |
-
ocr_result = _get_ocr_processor(ocr_model).extract_text(enhanced)
|
| 160 |
-
result["stages"]["ocr"] = {
|
| 161 |
-
"success": True,
|
| 162 |
-
"time_s": round(time.perf_counter() - t0, 3),
|
| 163 |
-
"element_count": ocr_result["element_count"],
|
| 164 |
-
"full_text": ocr_result["full_text"],
|
| 165 |
-
"classified": {
|
| 166 |
-
"dimensions": [e["text"] for e in ocr_result["classified"]["dimensions"]],
|
| 167 |
-
"room_names": [e["text"] for e in ocr_result["classified"]["room_names"]],
|
| 168 |
-
"numbers": [e["text"] for e in ocr_result["classified"]["numbers"]],
|
| 169 |
-
"labels": [e["text"] for e in ocr_result["classified"]["labels"]],
|
| 170 |
-
},
|
| 171 |
-
}
|
| 172 |
-
except Exception as exc:
|
| 173 |
-
ocr_result = None
|
| 174 |
-
result["stages"]["ocr"] = {"success": False, "error": str(exc)}
|
| 175 |
-
result["errors"].append(f"ocr: {exc}")
|
| 176 |
-
|
| 177 |
-
# Stage 4 — SVG (always run to verify generation)
|
| 178 |
-
t0 = time.perf_counter()
|
| 179 |
-
try:
|
| 180 |
-
svg = _svg.generate(lines, image.shape[1], image.shape[0], ocr_result)
|
| 181 |
-
result["stages"]["svg"] = {
|
| 182 |
-
"success": True,
|
| 183 |
-
"time_s": round(time.perf_counter() - t0, 3),
|
| 184 |
-
"svg_size_bytes": len(svg.encode()),
|
| 185 |
-
}
|
| 186 |
-
except Exception as exc:
|
| 187 |
-
result["stages"]["svg"] = {"success": False, "error": str(exc)}
|
| 188 |
-
result["errors"].append(f"svg: {exc}")
|
| 189 |
-
|
| 190 |
-
result["total_time_s"] = round(
|
| 191 |
-
sum(s.get("time_s", 0) for s in result["stages"].values()), 3
|
| 192 |
-
)
|
| 193 |
-
return result
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
# ---------------------------------------------------------------------------
|
| 197 |
-
# Accuracy scoring against ground truth
|
| 198 |
-
# ---------------------------------------------------------------------------
|
| 199 |
-
|
| 200 |
-
def score_against_ground_truth(
|
| 201 |
-
image_result: Dict[str, Any],
|
| 202 |
-
gt: Dict[str, Any],
|
| 203 |
-
) -> Dict[str, Any]:
|
| 204 |
-
"""
|
| 205 |
-
Compares pipeline output against a ground truth entry.
|
| 206 |
-
Returns a dict of accuracy scores for this image.
|
| 207 |
-
"""
|
| 208 |
-
scores: Dict[str, Any] = {}
|
| 209 |
-
|
| 210 |
-
ocr_stage = image_result["stages"].get("ocr", {})
|
| 211 |
-
line_stage = image_result["stages"].get("line_extraction", {})
|
| 212 |
-
|
| 213 |
-
# OCR — full text CER and WER
|
| 214 |
-
if ocr_stage.get("success") and "expected_text" in gt:
|
| 215 |
-
expected_str = " ".join(gt["expected_text"])
|
| 216 |
-
predicted_str = ocr_stage.get("full_text", "")
|
| 217 |
-
|
| 218 |
-
scores["ocr_cer"] = round(character_error_rate(predicted_str, expected_str), 4)
|
| 219 |
-
scores["ocr_wer"] = round(word_error_rate(predicted_str, expected_str), 4)
|
| 220 |
-
|
| 221 |
-
# Token-level recall/precision for key labels
|
| 222 |
-
all_tokens = (
|
| 223 |
-
ocr_stage["classified"]["dimensions"]
|
| 224 |
-
+ ocr_stage["classified"]["room_names"]
|
| 225 |
-
+ ocr_stage["classified"]["numbers"]
|
| 226 |
-
+ ocr_stage["classified"]["labels"]
|
| 227 |
-
)
|
| 228 |
-
rec = token_recall(all_tokens, gt["expected_text"])
|
| 229 |
-
prec = token_precision(all_tokens, gt["expected_text"])
|
| 230 |
-
scores["ocr_token_recall"] = round(rec, 4)
|
| 231 |
-
scores["ocr_token_precision"] = round(prec, 4)
|
| 232 |
-
scores["ocr_token_f1"] = round(f1(prec, rec), 4)
|
| 233 |
-
|
| 234 |
-
# Room name recall
|
| 235 |
-
if ocr_stage.get("success") and "expected_room_names" in gt:
|
| 236 |
-
rooms = ocr_stage["classified"]["room_names"]
|
| 237 |
-
scores["room_name_recall"] = round(
|
| 238 |
-
token_recall(rooms, gt["expected_room_names"]), 4
|
| 239 |
-
)
|
| 240 |
-
|
| 241 |
-
# Line count accuracy
|
| 242 |
-
if line_stage.get("success") and "expected_line_count_min" in gt:
|
| 243 |
-
scores["line_count_accuracy"] = round(
|
| 244 |
-
line_count_accuracy(
|
| 245 |
-
line_stage["lines_detected"],
|
| 246 |
-
gt["expected_line_count_min"],
|
| 247 |
-
gt["expected_line_count_max"],
|
| 248 |
-
),
|
| 249 |
-
4,
|
| 250 |
-
)
|
| 251 |
-
|
| 252 |
-
return scores
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
# ---------------------------------------------------------------------------
|
| 256 |
-
# Summary statistics
|
| 257 |
-
# ---------------------------------------------------------------------------
|
| 258 |
-
|
| 259 |
-
def aggregate(all_scores: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 260 |
-
"""Average all numeric score fields across images."""
|
| 261 |
-
if not all_scores:
|
| 262 |
-
return {}
|
| 263 |
-
keys = [k for k in all_scores[0] if isinstance(all_scores[0][k], float)]
|
| 264 |
-
agg: Dict[str, Any] = {}
|
| 265 |
-
for k in keys:
|
| 266 |
-
vals = [s[k] for s in all_scores if k in s]
|
| 267 |
-
if vals:
|
| 268 |
-
agg[f"avg_{k}"] = round(sum(vals) / len(vals), 4)
|
| 269 |
-
agg[f"min_{k}"] = round(min(vals), 4)
|
| 270 |
-
agg[f"max_{k}"] = round(max(vals), 4)
|
| 271 |
-
return agg
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
def print_summary(report: Dict[str, Any]) -> None:
|
| 275 |
-
"""Print a human-readable table to stdout."""
|
| 276 |
-
images = report["images"]
|
| 277 |
-
agg = report.get("aggregate", {})
|
| 278 |
-
metadata = report["metadata"]
|
| 279 |
-
|
| 280 |
-
print("\n" + "=" * 70)
|
| 281 |
-
print(f" Architectural Drawing AI — Benchmark Results")
|
| 282 |
-
print(f" {metadata['timestamp']} | {metadata['image_count']} image(s)")
|
| 283 |
-
print("=" * 70)
|
| 284 |
-
|
| 285 |
-
col = "{:<28} {:>7} {:>7} {:>8} {:>8}"
|
| 286 |
-
print(col.format("Image", "Lines", "OCR", "Time(s)", "Errors"))
|
| 287 |
-
print("-" * 70)
|
| 288 |
-
|
| 289 |
-
for img in images:
|
| 290 |
-
line_s = img["stages"].get("line_extraction", {})
|
| 291 |
-
ocr_s = img["stages"].get("ocr", {})
|
| 292 |
-
lines = line_s.get("lines_detected", "-") if line_s.get("success") else "ERR"
|
| 293 |
-
tokens = ocr_s.get("element_count", "-") if ocr_s.get("success") else "ERR"
|
| 294 |
-
errors = len(img.get("errors", []))
|
| 295 |
-
print(col.format(
|
| 296 |
-
img["filename"][:27],
|
| 297 |
-
str(lines),
|
| 298 |
-
str(tokens),
|
| 299 |
-
str(img.get("total_time_s", "-")),
|
| 300 |
-
str(errors) if errors else "",
|
| 301 |
-
))
|
| 302 |
-
|
| 303 |
-
if agg:
|
| 304 |
-
print("-" * 70)
|
| 305 |
-
print("\n Accuracy (averaged across all images with ground truth):\n")
|
| 306 |
-
metric_labels = {
|
| 307 |
-
"avg_ocr_cer": "OCR Character Error Rate (lower=better)",
|
| 308 |
-
"avg_ocr_wer": "OCR Word Error Rate (lower=better)",
|
| 309 |
-
"avg_ocr_token_f1": "OCR Token F1 (higher=better)",
|
| 310 |
-
"avg_ocr_token_recall": "OCR Token Recall",
|
| 311 |
-
"avg_ocr_token_precision": "OCR Token Precision",
|
| 312 |
-
"avg_room_name_recall": "Room Name Recall",
|
| 313 |
-
"avg_line_count_accuracy": "Line Count Accuracy",
|
| 314 |
-
}
|
| 315 |
-
for key, label in metric_labels.items():
|
| 316 |
-
if key in agg:
|
| 317 |
-
pct = f"{agg[key]*100:.1f}%"
|
| 318 |
-
print(f" {label:<45} {pct:>7}")
|
| 319 |
-
|
| 320 |
-
print("\n Results saved to:", report["metadata"].get("output_path", "—"))
|
| 321 |
-
print("=" * 70 + "\n")
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
# ---------------------------------------------------------------------------
|
| 325 |
-
# Entry point
|
| 326 |
-
# ---------------------------------------------------------------------------
|
| 327 |
-
|
| 328 |
-
def main() -> None:
|
| 329 |
-
parser = argparse.ArgumentParser(description="Benchmark the drawing pipeline")
|
| 330 |
-
parser.add_argument(
|
| 331 |
-
"--images", default=str(SAMPLE_DIR),
|
| 332 |
-
help="Directory of test images (default: benchmark/sample_images/)",
|
| 333 |
-
)
|
| 334 |
-
parser.add_argument(
|
| 335 |
-
"--gt", default=None,
|
| 336 |
-
help="Path to ground_truth.json for accuracy scoring",
|
| 337 |
-
)
|
| 338 |
-
parser.add_argument(
|
| 339 |
-
"--out", default=None,
|
| 340 |
-
help="Output JSON path (default: benchmark/results/benchmark_<ts>.json)",
|
| 341 |
-
)
|
| 342 |
-
parser.add_argument(
|
| 343 |
-
"-v", "--verbose", action="store_true",
|
| 344 |
-
help="Print per-image stage detail",
|
| 345 |
-
)
|
| 346 |
-
parser.add_argument(
|
| 347 |
-
"--ocr-model", default=None, choices=["tesseract", "paddleocr"],
|
| 348 |
-
help="OCR engine to benchmark (default: tesseract). Falls back to "
|
| 349 |
-
"tesseract if paddleocr is requested but unavailable.",
|
| 350 |
-
)
|
| 351 |
-
args = parser.parse_args()
|
| 352 |
-
|
| 353 |
-
images_dir = Path(args.images)
|
| 354 |
-
if not images_dir.exists():
|
| 355 |
-
print(f"ERROR: images directory not found: {images_dir}")
|
| 356 |
-
sys.exit(1)
|
| 357 |
-
|
| 358 |
-
image_paths = sorted(
|
| 359 |
-
p for p in images_dir.iterdir()
|
| 360 |
-
if p.suffix.lower() in SUPPORTED_EXTENSIONS
|
| 361 |
-
)
|
| 362 |
-
|
| 363 |
-
if not image_paths:
|
| 364 |
-
print(f"\nNo images found in {images_dir}")
|
| 365 |
-
print("Add JPEG/PNG architectural drawings to that directory and re-run.")
|
| 366 |
-
sys.exit(0)
|
| 367 |
-
|
| 368 |
-
ground_truth: Dict[str, Any] = {}
|
| 369 |
-
if args.gt:
|
| 370 |
-
gt_path = Path(args.gt)
|
| 371 |
-
if gt_path.exists():
|
| 372 |
-
with gt_path.open(encoding="utf-8") as f:
|
| 373 |
-
raw_gt = json.load(f)
|
| 374 |
-
# Strip comment keys
|
| 375 |
-
ground_truth = {k: v for k, v in raw_gt.items() if not k.startswith("_")}
|
| 376 |
-
print(f"Loaded ground truth for {len(ground_truth)} image(s).")
|
| 377 |
-
else:
|
| 378 |
-
print(f"WARNING: ground truth file not found: {gt_path}")
|
| 379 |
-
|
| 380 |
-
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 381 |
-
out_path = Path(args.out) if args.out else RESULTS_DIR / f"benchmark_{ts}.json"
|
| 382 |
-
|
| 383 |
-
print(f"\nProcessing {len(image_paths)} image(s) from {images_dir} …\n")
|
| 384 |
-
|
| 385 |
-
image_results = []
|
| 386 |
-
all_scores = []
|
| 387 |
-
|
| 388 |
-
for path in image_paths:
|
| 389 |
-
print(f" [{image_paths.index(path)+1}/{len(image_paths)}] {path.name} …", end=" ", flush=True)
|
| 390 |
-
result = process_image(path, ocr_model=args.ocr_model)
|
| 391 |
-
|
| 392 |
-
if "error" in result:
|
| 393 |
-
print(f"FAILED — {result['error']}")
|
| 394 |
-
else:
|
| 395 |
-
print(f"OK ({result['total_time_s']}s, "
|
| 396 |
-
f"{result['stages'].get('line_extraction', {}).get('lines_detected', '?')} lines, "
|
| 397 |
-
f"{result['stages'].get('ocr', {}).get('element_count', '?')} OCR tokens)")
|
| 398 |
-
|
| 399 |
-
if args.verbose:
|
| 400 |
-
for stage, data in result.get("stages", {}).items():
|
| 401 |
-
print(f" {stage}: {data}")
|
| 402 |
-
|
| 403 |
-
gt_entry = ground_truth.get(path.name)
|
| 404 |
-
if gt_entry:
|
| 405 |
-
scores = score_against_ground_truth(result, gt_entry)
|
| 406 |
-
result["accuracy"] = scores
|
| 407 |
-
all_scores.append(scores)
|
| 408 |
-
|
| 409 |
-
image_results.append(result)
|
| 410 |
-
|
| 411 |
-
report = {
|
| 412 |
-
"metadata": {
|
| 413 |
-
"timestamp": datetime.now().isoformat(),
|
| 414 |
-
"image_count": len(image_results),
|
| 415 |
-
"ground_truth": args.gt,
|
| 416 |
-
"output_path": str(out_path),
|
| 417 |
-
"pipeline_version": "1.0.0",
|
| 418 |
-
"ocr_model": args.ocr_model or "tesseract",
|
| 419 |
-
},
|
| 420 |
-
"images": image_results,
|
| 421 |
-
"aggregate": aggregate(all_scores) if all_scores else {},
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 425 |
-
with out_path.open("w", encoding="utf-8") as f:
|
| 426 |
-
json.dump(report, f, indent=2, ensure_ascii=False)
|
| 427 |
-
|
| 428 |
-
print_summary(report)
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
if __name__ == "__main__":
|
| 432 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/__init__.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Enhancement provider package.
|
| 3 |
-
Import `registry` for all provider access.
|
| 4 |
-
"""
|
| 5 |
-
from .registry import registry
|
| 6 |
-
from .base import EnhancementProvider
|
| 7 |
-
from .pipeline import build_preset_pipelines
|
| 8 |
-
|
| 9 |
-
# Register composite pipelines after all individual providers are loaded
|
| 10 |
-
build_preset_pipelines(registry)
|
| 11 |
-
|
| 12 |
-
__all__ = ["registry", "EnhancementProvider"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/base.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Abstract base class for all image enhancement providers.
|
| 3 |
-
|
| 4 |
-
Every provider must implement:
|
| 5 |
-
- name : str — unique slug used in the API (e.g. "classical", "real_esrgan")
|
| 6 |
-
- display_name: str — human-readable name shown in /api/info
|
| 7 |
-
- is_available: bool — False if required dependencies are not installed
|
| 8 |
-
- enhance(image) -> np.ndarray
|
| 9 |
-
|
| 10 |
-
Optional overrides:
|
| 11 |
-
- description : str
|
| 12 |
-
- enhance_to_grayscale(image) -> np.ndarray (for intermediate display)
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
from __future__ import annotations
|
| 16 |
-
from abc import ABC, abstractmethod
|
| 17 |
-
from typing import Optional
|
| 18 |
-
import numpy as np
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
class EnhancementProvider(ABC):
|
| 22 |
-
|
| 23 |
-
# ------------------------------------------------------------------ #
|
| 24 |
-
# Identity — subclasses set these as class attributes #
|
| 25 |
-
# ------------------------------------------------------------------ #
|
| 26 |
-
|
| 27 |
-
name: str = ""
|
| 28 |
-
display_name: str = ""
|
| 29 |
-
description: str = ""
|
| 30 |
-
|
| 31 |
-
# ------------------------------------------------------------------ #
|
| 32 |
-
# Availability #
|
| 33 |
-
# ------------------------------------------------------------------ #
|
| 34 |
-
|
| 35 |
-
@property
|
| 36 |
-
@abstractmethod
|
| 37 |
-
def is_available(self) -> bool:
|
| 38 |
-
"""
|
| 39 |
-
Return True only if all required packages are installed and any
|
| 40 |
-
required model weights are accessible. Called once at startup.
|
| 41 |
-
"""
|
| 42 |
-
|
| 43 |
-
@property
|
| 44 |
-
def unavailable_reason(self) -> Optional[str]:
|
| 45 |
-
"""Human-readable explanation shown in /api/info when unavailable."""
|
| 46 |
-
return None
|
| 47 |
-
|
| 48 |
-
# ------------------------------------------------------------------ #
|
| 49 |
-
# Core interface #
|
| 50 |
-
# ------------------------------------------------------------------ #
|
| 51 |
-
|
| 52 |
-
@abstractmethod
|
| 53 |
-
def enhance(self, image: np.ndarray) -> np.ndarray:
|
| 54 |
-
"""
|
| 55 |
-
Enhance the image.
|
| 56 |
-
|
| 57 |
-
Parameters
|
| 58 |
-
----------
|
| 59 |
-
image : np.ndarray
|
| 60 |
-
BGR or grayscale uint8 image as loaded by OpenCV.
|
| 61 |
-
|
| 62 |
-
Returns
|
| 63 |
-
-------
|
| 64 |
-
np.ndarray
|
| 65 |
-
Enhanced image — same dtype (uint8), same or different shape.
|
| 66 |
-
Must be suitable for direct input to LineExtractor and OCRProcessor.
|
| 67 |
-
"""
|
| 68 |
-
|
| 69 |
-
def enhance_to_grayscale(self, image: np.ndarray) -> np.ndarray:
|
| 70 |
-
"""
|
| 71 |
-
Return a grayscale (non-binary) enhanced image for display purposes.
|
| 72 |
-
Default: call enhance() and convert to grayscale if needed.
|
| 73 |
-
Override in subclasses if the provider has a natural grayscale output.
|
| 74 |
-
"""
|
| 75 |
-
import cv2
|
| 76 |
-
result = self.enhance(image)
|
| 77 |
-
if len(result.shape) == 3:
|
| 78 |
-
return cv2.cvtColor(result, cv2.COLOR_BGR2GRAY)
|
| 79 |
-
return result
|
| 80 |
-
|
| 81 |
-
# ------------------------------------------------------------------ #
|
| 82 |
-
# Metadata for /api/info #
|
| 83 |
-
# ------------------------------------------------------------------ #
|
| 84 |
-
|
| 85 |
-
def info(self) -> dict:
|
| 86 |
-
return {
|
| 87 |
-
"name": self.name,
|
| 88 |
-
"display_name": self.display_name,
|
| 89 |
-
"description": self.description,
|
| 90 |
-
"available": self.is_available,
|
| 91 |
-
"unavailable_reason": self.unavailable_reason if not self.is_available else None,
|
| 92 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/classical.py
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Classical OpenCV enhancement provider.
|
| 3 |
-
Always available — no extra dependencies beyond the existing requirements.
|
| 4 |
-
Wraps the existing ImageEnhancer pipeline.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
import numpy as np
|
| 9 |
-
|
| 10 |
-
from .base import EnhancementProvider
|
| 11 |
-
from processors.image_enhancer import ImageEnhancer
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class ClassicalCVProvider(EnhancementProvider):
|
| 15 |
-
|
| 16 |
-
name = "classical"
|
| 17 |
-
display_name = "Classical CV (OpenCV)"
|
| 18 |
-
description = (
|
| 19 |
-
"6-stage OpenCV pipeline: grayscale → NLM denoise → background removal "
|
| 20 |
-
"→ CLAHE contrast → adaptive threshold → morphological cleanup. "
|
| 21 |
-
"Fast, no GPU required, works on any hardware."
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
def __init__(self) -> None:
|
| 25 |
-
self._enhancer = ImageEnhancer()
|
| 26 |
-
|
| 27 |
-
@property
|
| 28 |
-
def is_available(self) -> bool:
|
| 29 |
-
return True # OpenCV is a hard dependency of the whole backend
|
| 30 |
-
|
| 31 |
-
def enhance(self, image: np.ndarray) -> np.ndarray:
|
| 32 |
-
return self._enhancer.enhance(image)
|
| 33 |
-
|
| 34 |
-
def enhance_to_grayscale(self, image: np.ndarray) -> np.ndarray:
|
| 35 |
-
return self._enhancer.enhance_grayscale(image)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/groq_guided.py
DELETED
|
@@ -1,288 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Groq AI-Guided Enhancement Provider
|
| 3 |
-
=====================================
|
| 4 |
-
Uses Groq's Llama 4 Scout vision model to assess the input drawing's
|
| 5 |
-
quality, type, and degradation level, then runs the classical OpenCV
|
| 6 |
-
pipeline with parameters tuned to that assessment.
|
| 7 |
-
|
| 8 |
-
What Groq contributes
|
| 9 |
-
---------------------
|
| 10 |
-
Groq is a text/vision inference API — it cannot modify pixel values.
|
| 11 |
-
Its role here is PARAMETER SELECTION:
|
| 12 |
-
|
| 13 |
-
• quality_score → how aggressively to denoise / boost contrast
|
| 14 |
-
• drawing_type → e.g. "detail" drawings need sharper lines
|
| 15 |
-
• ocr_difficulty → drive CLAHE strength for text regions
|
| 16 |
-
|
| 17 |
-
All actual image processing is performed by OpenCV via _TunableEnhancer,
|
| 18 |
-
a subclass of ImageEnhancer that accepts the parameter dict from Groq's
|
| 19 |
-
analysis instead of hardcoded constants.
|
| 20 |
-
|
| 21 |
-
Fallback
|
| 22 |
-
--------
|
| 23 |
-
If the Groq call fails for any reason (network error, rate limit, bad JSON),
|
| 24 |
-
the provider silently falls back to default parameters — equivalent to the
|
| 25 |
-
standard classical pipeline — so processing never fails.
|
| 26 |
-
"""
|
| 27 |
-
|
| 28 |
-
from __future__ import annotations
|
| 29 |
-
|
| 30 |
-
import base64
|
| 31 |
-
import json
|
| 32 |
-
import logging
|
| 33 |
-
import os
|
| 34 |
-
import warnings
|
| 35 |
-
from typing import Optional
|
| 36 |
-
|
| 37 |
-
import cv2
|
| 38 |
-
import numpy as np
|
| 39 |
-
|
| 40 |
-
from .base import EnhancementProvider
|
| 41 |
-
from processors.image_enhancer import ImageEnhancer
|
| 42 |
-
|
| 43 |
-
logger = logging.getLogger(__name__)
|
| 44 |
-
|
| 45 |
-
# ── Groq analysis prompt ──────────────────────────────────────────────────────
|
| 46 |
-
# Short, specific — we only need the fields we can act on.
|
| 47 |
-
_PARAM_PROMPT = (
|
| 48 |
-
"You are assessing a scanned architectural or engineering drawing for "
|
| 49 |
-
"automated image restoration. Respond ONLY with valid JSON (no markdown "
|
| 50 |
-
"fences) using exactly these fields:\n"
|
| 51 |
-
"{\n"
|
| 52 |
-
' "drawing_type": "floor plan | elevation | section | detail | '
|
| 53 |
-
'site plan | railway | electrical | mechanical | other",\n'
|
| 54 |
-
' "quality_score": <integer 1-10>,\n'
|
| 55 |
-
' "degradation": ["list from: faded, yellowed, stained, noisy, blurred, '
|
| 56 |
-
'folded, torn, foxing, compression artifacts"],\n'
|
| 57 |
-
' "has_color_fills": <true or false>,\n'
|
| 58 |
-
' "has_japanese_text": <true or false>,\n'
|
| 59 |
-
' "ocr_difficulty": "easy | moderate | hard | very hard"\n'
|
| 60 |
-
"}"
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
# ── Parameter mapping ─────────────────────────────────────────────────────────
|
| 65 |
-
|
| 66 |
-
def _default_params() -> dict:
|
| 67 |
-
"""Matches the hardcoded defaults in ImageEnhancer — safe fallback."""
|
| 68 |
-
return {
|
| 69 |
-
"bilateral_sigma": 40,
|
| 70 |
-
"clahe_clip": 2.0,
|
| 71 |
-
"content_clip": 3.0,
|
| 72 |
-
"sharpen_strength": 0.30,
|
| 73 |
-
"bg_brightness": 235,
|
| 74 |
-
"vivify_factor": 1.30,
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def _params_from_analysis(analysis: dict) -> dict:
|
| 79 |
-
"""
|
| 80 |
-
Convert Groq's quality analysis into a parameter dict.
|
| 81 |
-
|
| 82 |
-
Quality tier → base settings
|
| 83 |
-
OCR difficulty → lifts CLAHE in content zones
|
| 84 |
-
Drawing type → increases sharpening for detail/section drawings
|
| 85 |
-
"""
|
| 86 |
-
params = _default_params()
|
| 87 |
-
quality = int(analysis.get("quality_score") or 5)
|
| 88 |
-
ocr = (analysis.get("ocr_difficulty") or "moderate").lower()
|
| 89 |
-
dtype = (analysis.get("drawing_type") or "").lower()
|
| 90 |
-
|
| 91 |
-
# ── Quality tier ──
|
| 92 |
-
if quality <= 3:
|
| 93 |
-
# Severely degraded: maximum denoising + contrast
|
| 94 |
-
params.update(bilateral_sigma=55, clahe_clip=3.5,
|
| 95 |
-
content_clip=4.5, sharpen_strength=0.45, bg_brightness=228)
|
| 96 |
-
elif quality <= 6:
|
| 97 |
-
# Moderately degraded: standard-plus settings
|
| 98 |
-
params.update(bilateral_sigma=40, clahe_clip=2.5,
|
| 99 |
-
content_clip=3.5, sharpen_strength=0.35, bg_brightness=232)
|
| 100 |
-
elif quality <= 8:
|
| 101 |
-
# Good: default settings
|
| 102 |
-
params.update(bilateral_sigma=30, clahe_clip=2.0,
|
| 103 |
-
content_clip=3.0, sharpen_strength=0.30, bg_brightness=236)
|
| 104 |
-
else:
|
| 105 |
-
# Excellent: minimal processing to avoid over-cooking
|
| 106 |
-
params.update(bilateral_sigma=22, clahe_clip=1.5,
|
| 107 |
-
content_clip=2.5, sharpen_strength=0.20, bg_brightness=242)
|
| 108 |
-
|
| 109 |
-
# ── OCR difficulty modifier ──
|
| 110 |
-
if ocr in ("hard", "very hard"):
|
| 111 |
-
params["clahe_clip"] = min(params["clahe_clip"] + 0.5, 5.0)
|
| 112 |
-
params["content_clip"] = min(params["content_clip"] + 0.5, 6.0)
|
| 113 |
-
if ocr == "very hard":
|
| 114 |
-
params["bilateral_sigma"] = min(params["bilateral_sigma"] + 10, 65)
|
| 115 |
-
|
| 116 |
-
# ── Drawing type modifier ──
|
| 117 |
-
if any(t in dtype for t in ("detail", "section")):
|
| 118 |
-
# Precise linework drawings benefit from stronger sharpening
|
| 119 |
-
params["sharpen_strength"] = min(params["sharpen_strength"] + 0.10, 0.60)
|
| 120 |
-
|
| 121 |
-
logger.info(
|
| 122 |
-
"Groq-guided params — quality=%d ocr=%s type=%s "
|
| 123 |
-
"σ=%d clahe=%.1f content_clahe=%.1f sharpen=%.2f",
|
| 124 |
-
quality, ocr, dtype or "unknown",
|
| 125 |
-
params["bilateral_sigma"], params["clahe_clip"],
|
| 126 |
-
params["content_clip"], params["sharpen_strength"],
|
| 127 |
-
)
|
| 128 |
-
return params
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
# ── Tunable pipeline ──────────────────────────────────────────────────────────
|
| 132 |
-
|
| 133 |
-
class _TunableEnhancer(ImageEnhancer):
|
| 134 |
-
"""
|
| 135 |
-
ImageEnhancer subclass where the four parametric stages read their
|
| 136 |
-
settings from a dict instead of hardcoded constants.
|
| 137 |
-
|
| 138 |
-
Non-parametric stages (crop, upscale, border, deskew, background
|
| 139 |
-
normalisation, speck removal) are inherited unchanged.
|
| 140 |
-
"""
|
| 141 |
-
|
| 142 |
-
def __init__(self, params: dict) -> None:
|
| 143 |
-
self._p = params
|
| 144 |
-
|
| 145 |
-
def _bilateral_denoise(self, image: np.ndarray) -> np.ndarray:
|
| 146 |
-
s = self._p["bilateral_sigma"]
|
| 147 |
-
d1 = cv2.bilateralFilter(image, d=9, sigmaColor=s, sigmaSpace=15)
|
| 148 |
-
return cv2.bilateralFilter(d1, d=9, sigmaColor=max(10, s // 2), sigmaSpace=10)
|
| 149 |
-
|
| 150 |
-
def _enhance_contrast(self, image: np.ndarray) -> np.ndarray:
|
| 151 |
-
clip = self._p["clahe_clip"]
|
| 152 |
-
if len(image.shape) == 2:
|
| 153 |
-
return cv2.createCLAHE(clipLimit=clip, tileGridSize=(8, 8)).apply(image)
|
| 154 |
-
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
|
| 155 |
-
l, a, b = cv2.split(lab)
|
| 156 |
-
l = cv2.createCLAHE(clipLimit=clip, tileGridSize=(8, 8)).apply(l)
|
| 157 |
-
return cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR)
|
| 158 |
-
|
| 159 |
-
def _content_enhance(self, image: np.ndarray) -> np.ndarray:
|
| 160 |
-
clip = self._p["content_clip"]
|
| 161 |
-
gray = image if len(image.shape) == 2 else cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 162 |
-
content_px = (gray < 200).astype(np.uint8)
|
| 163 |
-
kern = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (21, 21))
|
| 164 |
-
content_zone = cv2.dilate(content_px, kern).astype(bool)
|
| 165 |
-
clahe = cv2.createCLAHE(clipLimit=clip, tileGridSize=(8, 8))
|
| 166 |
-
if len(image.shape) == 2:
|
| 167 |
-
return np.where(content_zone, clahe.apply(image), image).astype(np.uint8)
|
| 168 |
-
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
|
| 169 |
-
l, a, b = cv2.split(lab)
|
| 170 |
-
l_out = np.where(content_zone, clahe.apply(l), l).astype(np.uint8)
|
| 171 |
-
return cv2.cvtColor(cv2.merge([l_out, a, b]), cv2.COLOR_LAB2BGR)
|
| 172 |
-
|
| 173 |
-
def _sharpen_linework(self, image: np.ndarray) -> np.ndarray:
|
| 174 |
-
strength = self._p["sharpen_strength"]
|
| 175 |
-
gray = image if len(image.shape) == 2 else cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 176 |
-
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
|
| 177 |
-
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
|
| 178 |
-
grad = np.sqrt(gx ** 2 + gy ** 2)
|
| 179 |
-
edge = ((gray < 180) & (grad > 15)).astype(np.uint8)
|
| 180 |
-
kern = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 181 |
-
zone = cv2.dilate(edge, kern).astype(bool)
|
| 182 |
-
blur = cv2.GaussianBlur(image, (3, 3), sigmaX=0.8)
|
| 183 |
-
sharpened = cv2.addWeighted(image, 1.0 + strength, blur, -strength, 0)
|
| 184 |
-
if len(image.shape) == 3:
|
| 185 |
-
mask3 = np.stack([zone, zone, zone], axis=2)
|
| 186 |
-
return np.where(mask3, sharpened, image).astype(np.uint8)
|
| 187 |
-
return np.where(zone, sharpened, image).astype(np.uint8)
|
| 188 |
-
|
| 189 |
-
def _clean_background(self, image: np.ndarray) -> np.ndarray:
|
| 190 |
-
thresh = self._p["bg_brightness"]
|
| 191 |
-
gray = image if len(image.shape) == 2 else cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 192 |
-
gray_f = gray.astype(np.float32)
|
| 193 |
-
variance = np.maximum(
|
| 194 |
-
cv2.blur(gray_f ** 2, (9, 9)) - cv2.blur(gray_f, (9, 9)) ** 2, 0.0
|
| 195 |
-
)
|
| 196 |
-
bg_mask = (gray > thresh) & (variance < 60)
|
| 197 |
-
result = image.copy()
|
| 198 |
-
if len(image.shape) == 2:
|
| 199 |
-
result[bg_mask] = 255
|
| 200 |
-
else:
|
| 201 |
-
result[bg_mask] = (255, 255, 255)
|
| 202 |
-
logger.info("BG cleanup (thresh=%d): %d px → white", thresh, int(bg_mask.sum()))
|
| 203 |
-
return result
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
# ── Provider class ────────────────────────────────────────────────────────────
|
| 207 |
-
|
| 208 |
-
class GroqGuidedProvider(EnhancementProvider):
|
| 209 |
-
"""
|
| 210 |
-
Enhancement provider that uses Groq's vision model to assess the
|
| 211 |
-
drawing and then runs a parameter-tuned classical CV pipeline.
|
| 212 |
-
"""
|
| 213 |
-
|
| 214 |
-
name = "groq_guided"
|
| 215 |
-
display_name = "Groq AI-Guided (Llama 4 Scout + OpenCV)"
|
| 216 |
-
description = (
|
| 217 |
-
"Groq Llama 4 Scout vision assesses drawing quality, type, and "
|
| 218 |
-
"degradation — then a tuned OpenCV pipeline applies the right "
|
| 219 |
-
"enhancement strength. Requires GROQ_API_KEY in backend/.env."
|
| 220 |
-
)
|
| 221 |
-
|
| 222 |
-
@property
|
| 223 |
-
def is_available(self) -> bool:
|
| 224 |
-
return bool(os.getenv("GROQ_API_KEY", "").strip())
|
| 225 |
-
|
| 226 |
-
@property
|
| 227 |
-
def unavailable_reason(self) -> Optional[str]:
|
| 228 |
-
return None if self.is_available else "GROQ_API_KEY not set in backend/.env"
|
| 229 |
-
|
| 230 |
-
def enhance(self, image: np.ndarray) -> np.ndarray:
|
| 231 |
-
analysis = self._analyze(image)
|
| 232 |
-
params = _params_from_analysis(analysis)
|
| 233 |
-
return _TunableEnhancer(params).enhance(image)
|
| 234 |
-
|
| 235 |
-
# ── Groq call ──────────────────────────────────────────────────────────
|
| 236 |
-
|
| 237 |
-
def _analyze(self, image: np.ndarray) -> dict:
|
| 238 |
-
"""
|
| 239 |
-
Send an 800 px thumbnail to Groq for quality assessment.
|
| 240 |
-
Returns empty dict (→ default params) on any failure.
|
| 241 |
-
"""
|
| 242 |
-
thumb = self._thumbnail(image, 800)
|
| 243 |
-
_, jpeg = cv2.imencode(".jpg", thumb, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
| 244 |
-
b64 = base64.b64encode(jpeg.tobytes()).decode()
|
| 245 |
-
|
| 246 |
-
try:
|
| 247 |
-
raw = self._call_groq(b64)
|
| 248 |
-
text = raw.strip()
|
| 249 |
-
if text.startswith("```"):
|
| 250 |
-
text = text.split("```")[1].lstrip("json").strip()
|
| 251 |
-
result = json.loads(text)
|
| 252 |
-
logger.info("Groq analysis: %s", result)
|
| 253 |
-
return result
|
| 254 |
-
except Exception as exc:
|
| 255 |
-
logger.warning("Groq analysis failed (%s) — using default params", exc)
|
| 256 |
-
return {}
|
| 257 |
-
|
| 258 |
-
def _call_groq(self, b64: str) -> str:
|
| 259 |
-
import httpx
|
| 260 |
-
from groq import Groq
|
| 261 |
-
|
| 262 |
-
api_key = os.getenv("GROQ_API_KEY", "").strip()
|
| 263 |
-
with warnings.catch_warnings():
|
| 264 |
-
warnings.simplefilter("ignore")
|
| 265 |
-
client = Groq(api_key=api_key, http_client=httpx.Client(verify=False)) # noqa: S501 # NOSONAR
|
| 266 |
-
|
| 267 |
-
resp = client.chat.completions.create(
|
| 268 |
-
model="meta-llama/llama-4-scout-17b-16e-instruct",
|
| 269 |
-
max_tokens=256,
|
| 270 |
-
messages=[{
|
| 271 |
-
"role": "user",
|
| 272 |
-
"content": [
|
| 273 |
-
{"type": "image_url",
|
| 274 |
-
"image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
|
| 275 |
-
{"type": "text", "text": _PARAM_PROMPT},
|
| 276 |
-
],
|
| 277 |
-
}],
|
| 278 |
-
)
|
| 279 |
-
return resp.choices[0].message.content
|
| 280 |
-
|
| 281 |
-
@staticmethod
|
| 282 |
-
def _thumbnail(image: np.ndarray, max_side: int) -> np.ndarray:
|
| 283 |
-
h, w = image.shape[:2]
|
| 284 |
-
if max(h, w) <= max_side:
|
| 285 |
-
return image
|
| 286 |
-
scale = max_side / max(h, w)
|
| 287 |
-
return cv2.resize(image, (int(w * scale), int(h * scale)),
|
| 288 |
-
interpolation=cv2.INTER_AREA)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/huggingface.py
DELETED
|
@@ -1,130 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
HuggingFace enhancement provider.
|
| 3 |
-
===================================
|
| 4 |
-
Loads any image-to-image model from HuggingFace Hub and applies it as an
|
| 5 |
-
enhancement step before classical binarisation.
|
| 6 |
-
|
| 7 |
-
Recommended models for architectural drawing restoration:
|
| 8 |
-
- "microsoft/swin2SR-compressed-SR-x2-64" (lightweight super-resolution)
|
| 9 |
-
- "caidas/swin2SR-realworld-sr-x4-64-bsrgan-psnr" (real-world degradation)
|
| 10 |
-
- "eugenesiow/srcnn-scale2" (fast, minimal memory)
|
| 11 |
-
|
| 12 |
-
Installation
|
| 13 |
-
------------
|
| 14 |
-
pip install transformers Pillow torch
|
| 15 |
-
|
| 16 |
-
Usage (via API)
|
| 17 |
-
---------------
|
| 18 |
-
POST /api/process
|
| 19 |
-
{
|
| 20 |
-
"enhancement_model": "huggingface",
|
| 21 |
-
"hf_model_id": "microsoft/swin2SR-compressed-SR-x2-64"
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
Or set a default via env var:
|
| 25 |
-
HF_ENHANCEMENT_MODEL=microsoft/swin2SR-compressed-SR-x2-64
|
| 26 |
-
"""
|
| 27 |
-
|
| 28 |
-
from __future__ import annotations
|
| 29 |
-
import logging
|
| 30 |
-
import os
|
| 31 |
-
from typing import Optional
|
| 32 |
-
|
| 33 |
-
import numpy as np
|
| 34 |
-
|
| 35 |
-
from .base import EnhancementProvider
|
| 36 |
-
|
| 37 |
-
logger = logging.getLogger(__name__)
|
| 38 |
-
|
| 39 |
-
_HF_AVAILABLE = False
|
| 40 |
-
_HF_ERROR: Optional[str] = None
|
| 41 |
-
|
| 42 |
-
try:
|
| 43 |
-
from transformers import pipeline as hf_pipeline # noqa: F401
|
| 44 |
-
from PIL import Image # noqa: F401
|
| 45 |
-
_HF_AVAILABLE = True
|
| 46 |
-
except ImportError as _e:
|
| 47 |
-
_HF_ERROR = f"transformers / Pillow not installed: {_e}"
|
| 48 |
-
|
| 49 |
-
_DEFAULT_HF_MODEL = os.environ.get(
|
| 50 |
-
"HF_ENHANCEMENT_MODEL",
|
| 51 |
-
"caidas/swin2SR-realworld-sr-x4-64-bsrgan-psnr",
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
class HuggingFaceProvider(EnhancementProvider):
|
| 56 |
-
|
| 57 |
-
name = "huggingface"
|
| 58 |
-
display_name = "HuggingFace (configurable model)"
|
| 59 |
-
description = (
|
| 60 |
-
"Applies any image-to-image HuggingFace model as a pre-processing step. "
|
| 61 |
-
"Pass 'hf_model_id' in the request to select a model, or set the "
|
| 62 |
-
"HF_ENHANCEMENT_MODEL env var. Falls back to classical binarisation after "
|
| 63 |
-
"the model runs. Install: pip install transformers Pillow torch"
|
| 64 |
-
)
|
| 65 |
-
|
| 66 |
-
def __init__(self, model_id: Optional[str] = None) -> None:
|
| 67 |
-
self._model_id = model_id or _DEFAULT_HF_MODEL
|
| 68 |
-
self._pipe = None # lazy-loaded
|
| 69 |
-
|
| 70 |
-
@property
|
| 71 |
-
def is_available(self) -> bool:
|
| 72 |
-
return _HF_AVAILABLE
|
| 73 |
-
|
| 74 |
-
@property
|
| 75 |
-
def unavailable_reason(self) -> Optional[str]:
|
| 76 |
-
return _HF_ERROR
|
| 77 |
-
|
| 78 |
-
def with_model(self, model_id: str) -> "HuggingFaceProvider":
|
| 79 |
-
"""Return a new provider instance using the specified model."""
|
| 80 |
-
return HuggingFaceProvider(model_id=model_id)
|
| 81 |
-
|
| 82 |
-
def enhance(self, image: np.ndarray) -> np.ndarray:
|
| 83 |
-
import cv2
|
| 84 |
-
from PIL import Image as PILImage
|
| 85 |
-
from processors.image_enhancer import ImageEnhancer
|
| 86 |
-
|
| 87 |
-
pipe = self._get_pipeline()
|
| 88 |
-
|
| 89 |
-
# Convert to PIL RGB for the transformer pipeline
|
| 90 |
-
if len(image.shape) == 2:
|
| 91 |
-
pil_img = PILImage.fromarray(image).convert("RGB")
|
| 92 |
-
else:
|
| 93 |
-
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 94 |
-
pil_img = PILImage.fromarray(rgb)
|
| 95 |
-
|
| 96 |
-
logger.info("HuggingFace enhancing with model: %s", self._model_id)
|
| 97 |
-
result = pipe(pil_img)
|
| 98 |
-
|
| 99 |
-
# Pipeline output varies by model — handle both PIL and list-of-dict
|
| 100 |
-
if isinstance(result, list):
|
| 101 |
-
out_pil = result[0].get("image", pil_img) if isinstance(result[0], dict) else result[0]
|
| 102 |
-
elif hasattr(result, "images"):
|
| 103 |
-
out_pil = result.images[0]
|
| 104 |
-
else:
|
| 105 |
-
out_pil = result
|
| 106 |
-
|
| 107 |
-
out_np = np.array(out_pil.convert("L")) # grayscale uint8
|
| 108 |
-
|
| 109 |
-
# Finish with classical binarisation so output is compatible
|
| 110 |
-
# with downstream LineExtractor + OCR processors
|
| 111 |
-
enh = ImageEnhancer()
|
| 112 |
-
contrast = enh._enhance_contrast(out_np)
|
| 113 |
-
binary = enh._binarize(contrast)
|
| 114 |
-
return enh._morphological_cleanup(binary)
|
| 115 |
-
|
| 116 |
-
def _get_pipeline(self):
|
| 117 |
-
if self._pipe is not None:
|
| 118 |
-
return self._pipe
|
| 119 |
-
from transformers import pipeline as hf_pipeline
|
| 120 |
-
logger.info("Loading HuggingFace pipeline: %s", self._model_id)
|
| 121 |
-
self._pipe = hf_pipeline(
|
| 122 |
-
"image-to-image",
|
| 123 |
-
model=self._model_id,
|
| 124 |
-
)
|
| 125 |
-
return self._pipe
|
| 126 |
-
|
| 127 |
-
def info(self) -> dict:
|
| 128 |
-
base = super().info()
|
| 129 |
-
base["active_model"] = self._model_id
|
| 130 |
-
return base
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/huggingface_api.py
DELETED
|
@@ -1,140 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
HuggingFace Serverless Inference API — Cloud Enhancement Provider
|
| 3 |
-
==================================================================
|
| 4 |
-
Sends the image to a HuggingFace-hosted super-resolution model via their
|
| 5 |
-
free Serverless Inference API.
|
| 6 |
-
|
| 7 |
-
Setup (free)
|
| 8 |
-
------------
|
| 9 |
-
1. Create a free account at https://huggingface.co
|
| 10 |
-
2. Go to https://huggingface.co/settings/tokens → Create new token
|
| 11 |
-
3. Under "User Permissions" enable "Make calls to Inference Providers"
|
| 12 |
-
4. Add to backend/.env:
|
| 13 |
-
HF_API_TOKEN=hf_...
|
| 14 |
-
|
| 15 |
-
Optional — override the default model:
|
| 16 |
-
HF_API_MODEL=eugenesiow/srcnn-scale2
|
| 17 |
-
|
| 18 |
-
Default model
|
| 19 |
-
-------------
|
| 20 |
-
caidas/swin2SR-realworld-sr-x4-64-bsrgan-psnr
|
| 21 |
-
4x super-resolution trained on real-world degraded images.
|
| 22 |
-
Works well on scanned/photocopied documents.
|
| 23 |
-
|
| 24 |
-
Limitations of the free tier
|
| 25 |
-
-----------------------------
|
| 26 |
-
~10 requests/minute on shared infrastructure
|
| 27 |
-
Max image size: ~4 MB — provider auto-resizes before sending
|
| 28 |
-
SSL verification is disabled to work through corporate proxies
|
| 29 |
-
"""
|
| 30 |
-
|
| 31 |
-
from __future__ import annotations
|
| 32 |
-
import io
|
| 33 |
-
import logging
|
| 34 |
-
import os
|
| 35 |
-
import warnings
|
| 36 |
-
from typing import Optional
|
| 37 |
-
|
| 38 |
-
import numpy as np
|
| 39 |
-
|
| 40 |
-
from .base import EnhancementProvider
|
| 41 |
-
|
| 42 |
-
logger = logging.getLogger(__name__)
|
| 43 |
-
|
| 44 |
-
_DEFAULT_MODEL = os.environ.get(
|
| 45 |
-
"HF_API_MODEL",
|
| 46 |
-
"caidas/swin2SR-realworld-sr-x4-64-bsrgan-psnr",
|
| 47 |
-
)
|
| 48 |
-
|
| 49 |
-
# New HuggingFace Inference API endpoint (replaces api-inference.huggingface.co)
|
| 50 |
-
_HF_ROUTER_BASE = "https://router.huggingface.co/hf-inference/models"
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
class HuggingFaceAPIProvider(EnhancementProvider):
|
| 54 |
-
|
| 55 |
-
name = "huggingface_api"
|
| 56 |
-
display_name = "HuggingFace Cloud API (free)"
|
| 57 |
-
description = (
|
| 58 |
-
"Cloud super-resolution via HuggingFace Serverless Inference API. "
|
| 59 |
-
"Free account required — add HF_API_TOKEN to backend/.env with "
|
| 60 |
-
"'Make calls to Inference Providers' permission enabled."
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
def __init__(self, model_id: Optional[str] = None) -> None:
|
| 64 |
-
self._model_id = model_id or _DEFAULT_MODEL
|
| 65 |
-
|
| 66 |
-
@property
|
| 67 |
-
def is_available(self) -> bool:
|
| 68 |
-
return bool(os.environ.get("HF_API_TOKEN", "").strip())
|
| 69 |
-
|
| 70 |
-
@property
|
| 71 |
-
def unavailable_reason(self) -> Optional[str]:
|
| 72 |
-
if not self.is_available:
|
| 73 |
-
return (
|
| 74 |
-
"HF_API_TOKEN not set. "
|
| 75 |
-
"Get a free token at https://huggingface.co/settings/tokens "
|
| 76 |
-
"(enable 'Make calls to Inference Providers') "
|
| 77 |
-
"and add it to backend/.env"
|
| 78 |
-
)
|
| 79 |
-
return None
|
| 80 |
-
|
| 81 |
-
def with_model(self, model_id: str) -> "HuggingFaceAPIProvider":
|
| 82 |
-
return HuggingFaceAPIProvider(model_id=model_id)
|
| 83 |
-
|
| 84 |
-
def enhance(self, image: np.ndarray) -> np.ndarray:
|
| 85 |
-
import cv2
|
| 86 |
-
import requests
|
| 87 |
-
from PIL import Image as PILImage
|
| 88 |
-
from processors.image_enhancer import ImageEnhancer
|
| 89 |
-
|
| 90 |
-
token = os.environ.get("HF_API_TOKEN", "").strip()
|
| 91 |
-
url = f"{_HF_ROUTER_BASE}/{self._model_id}"
|
| 92 |
-
|
| 93 |
-
# Convert to BGR then resize for free-tier payload limit (~4 MB)
|
| 94 |
-
bgr = image if len(image.shape) == 3 else cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
| 95 |
-
h, w = bgr.shape[:2]
|
| 96 |
-
if max(h, w) > 1500:
|
| 97 |
-
scale = 1500 / max(h, w)
|
| 98 |
-
bgr = cv2.resize(bgr, (int(w * scale), int(h * scale)),
|
| 99 |
-
interpolation=cv2.INTER_AREA)
|
| 100 |
-
|
| 101 |
-
_, enc = cv2.imencode(".jpg", bgr, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
| 102 |
-
img_bytes = enc.tobytes()
|
| 103 |
-
|
| 104 |
-
logger.info("HF API request: model=%s input=%dx%d size=%.1f KB",
|
| 105 |
-
self._model_id, bgr.shape[1], bgr.shape[0], len(img_bytes) / 1024)
|
| 106 |
-
|
| 107 |
-
# verify=False to work through corporate proxies with self-signed certs
|
| 108 |
-
with warnings.catch_warnings():
|
| 109 |
-
warnings.simplefilter("ignore")
|
| 110 |
-
resp = requests.post(
|
| 111 |
-
url,
|
| 112 |
-
headers={
|
| 113 |
-
"Authorization": f"Bearer {token}",
|
| 114 |
-
"Content-Type": "image/jpeg",
|
| 115 |
-
},
|
| 116 |
-
data=img_bytes,
|
| 117 |
-
verify=False, # noqa: S501 — intentional: corporate proxy uses self-signed cert
|
| 118 |
-
timeout=120,
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
if resp.status_code != 200:
|
| 122 |
-
raise RuntimeError(
|
| 123 |
-
f"HuggingFace API {resp.status_code}: {resp.text[:400]}"
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
out_pil = PILImage.open(io.BytesIO(resp.content)).convert("RGB")
|
| 127 |
-
out_bgr = cv2.cvtColor(np.array(out_pil), cv2.COLOR_RGB2BGR)
|
| 128 |
-
logger.info("HF API response: %dx%d", out_bgr.shape[1], out_bgr.shape[0])
|
| 129 |
-
|
| 130 |
-
# Binarise so output is compatible with LineExtractor + OCRProcessor
|
| 131 |
-
enh = ImageEnhancer()
|
| 132 |
-
gray = cv2.cvtColor(out_bgr, cv2.COLOR_BGR2GRAY)
|
| 133 |
-
contrast = enh._enhance_contrast(gray)
|
| 134 |
-
binary = enh._binarize(contrast)
|
| 135 |
-
return enh._morphological_cleanup(binary)
|
| 136 |
-
|
| 137 |
-
def info(self) -> dict:
|
| 138 |
-
base = super().info()
|
| 139 |
-
base["active_model"] = self._model_id
|
| 140 |
-
return base
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/pipeline.py
DELETED
|
@@ -1,124 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Enhancement Pipeline
|
| 3 |
-
====================
|
| 4 |
-
Chains multiple providers in sequence. The output of each provider becomes
|
| 5 |
-
the input of the next. Useful for combining AI super-resolution with
|
| 6 |
-
classical binarisation, or for running experimental multi-stage workflows.
|
| 7 |
-
|
| 8 |
-
Example chains defined here:
|
| 9 |
-
"ai_then_classical" — Real-ESRGAN upscale → classical binarise
|
| 10 |
-
"hf_then_classical" — HuggingFace model → classical binarise
|
| 11 |
-
|
| 12 |
-
Custom chains can be built at runtime:
|
| 13 |
-
from processors.enhancement.pipeline import EnhancementPipeline
|
| 14 |
-
from processors.enhancement.registry import registry
|
| 15 |
-
|
| 16 |
-
pipe = EnhancementPipeline([
|
| 17 |
-
registry.get("real_esrgan"),
|
| 18 |
-
registry.get("classical"),
|
| 19 |
-
])
|
| 20 |
-
result = pipe.enhance(image)
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
-
from __future__ import annotations
|
| 24 |
-
import logging
|
| 25 |
-
from typing import List, Optional
|
| 26 |
-
|
| 27 |
-
import numpy as np
|
| 28 |
-
|
| 29 |
-
from .base import EnhancementProvider
|
| 30 |
-
|
| 31 |
-
logger = logging.getLogger(__name__)
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
class EnhancementPipeline(EnhancementProvider):
|
| 35 |
-
"""
|
| 36 |
-
Meta-provider that runs a list of providers in sequence.
|
| 37 |
-
Reports as available only if ALL constituent providers are available.
|
| 38 |
-
"""
|
| 39 |
-
|
| 40 |
-
def __init__(
|
| 41 |
-
self,
|
| 42 |
-
providers: List[EnhancementProvider],
|
| 43 |
-
name: str = "pipeline",
|
| 44 |
-
display_name: str = "Custom Pipeline",
|
| 45 |
-
description: str = "",
|
| 46 |
-
) -> None:
|
| 47 |
-
if not providers:
|
| 48 |
-
raise ValueError("EnhancementPipeline requires at least one provider")
|
| 49 |
-
self._providers = providers
|
| 50 |
-
self.name = name
|
| 51 |
-
self.display_name = display_name
|
| 52 |
-
self.description = description or " → ".join(p.display_name for p in providers)
|
| 53 |
-
|
| 54 |
-
@property
|
| 55 |
-
def is_available(self) -> bool:
|
| 56 |
-
return all(p.is_available for p in self._providers)
|
| 57 |
-
|
| 58 |
-
@property
|
| 59 |
-
def unavailable_reason(self) -> Optional[str]:
|
| 60 |
-
reasons = [
|
| 61 |
-
f"{p.name}: {p.unavailable_reason}"
|
| 62 |
-
for p in self._providers
|
| 63 |
-
if not p.is_available
|
| 64 |
-
]
|
| 65 |
-
return "; ".join(reasons) if reasons else None
|
| 66 |
-
|
| 67 |
-
def enhance(self, image: np.ndarray) -> np.ndarray:
|
| 68 |
-
result = image
|
| 69 |
-
for provider in self._providers:
|
| 70 |
-
logger.info("Pipeline stage: %s", provider.name)
|
| 71 |
-
result = provider.enhance(result)
|
| 72 |
-
return result
|
| 73 |
-
|
| 74 |
-
def info(self) -> dict:
|
| 75 |
-
base = super().info()
|
| 76 |
-
base["stages"] = [p.name for p in self._providers]
|
| 77 |
-
return base
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
# ---------------------------------------------------------------------------
|
| 81 |
-
# Pre-built named chains registered via the registry
|
| 82 |
-
# ---------------------------------------------------------------------------
|
| 83 |
-
|
| 84 |
-
def build_preset_pipelines(registry) -> None:
|
| 85 |
-
"""
|
| 86 |
-
Register composite pipeline presets into an existing registry.
|
| 87 |
-
Called once after the registry has loaded all individual providers.
|
| 88 |
-
|
| 89 |
-
This is intentionally separated so the registry doesn't need to
|
| 90 |
-
import pipeline — avoiding circular imports.
|
| 91 |
-
"""
|
| 92 |
-
try:
|
| 93 |
-
esrgan = registry._providers.get("real_esrgan")
|
| 94 |
-
hf = registry._providers.get("huggingface")
|
| 95 |
-
classical = registry._providers.get("classical")
|
| 96 |
-
|
| 97 |
-
if esrgan and classical:
|
| 98 |
-
pipe = EnhancementPipeline(
|
| 99 |
-
providers=[esrgan, classical],
|
| 100 |
-
name="ai_then_classical",
|
| 101 |
-
display_name="AI Super-Resolution + Classical Binarise",
|
| 102 |
-
description=(
|
| 103 |
-
"Real-ESRGAN 4× upscale and denoise, followed by classical "
|
| 104 |
-
"adaptive threshold binarisation. Best for degraded photographic scans."
|
| 105 |
-
),
|
| 106 |
-
)
|
| 107 |
-
registry._providers[pipe.name] = pipe
|
| 108 |
-
logger.info("Pipeline registered: %s", pipe.name)
|
| 109 |
-
|
| 110 |
-
if hf and classical:
|
| 111 |
-
pipe = EnhancementPipeline(
|
| 112 |
-
providers=[hf, classical],
|
| 113 |
-
name="hf_then_classical",
|
| 114 |
-
display_name="HuggingFace Model + Classical Binarise",
|
| 115 |
-
description=(
|
| 116 |
-
"Configurable HuggingFace image model followed by classical "
|
| 117 |
-
"binarisation. Set HF_ENHANCEMENT_MODEL env var to choose model."
|
| 118 |
-
),
|
| 119 |
-
)
|
| 120 |
-
registry._providers[pipe.name] = pipe
|
| 121 |
-
logger.info("Pipeline registered: %s", pipe.name)
|
| 122 |
-
|
| 123 |
-
except Exception as exc:
|
| 124 |
-
logger.warning("Failed to build preset pipelines: %s", exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/real_esrgan.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Real-ESRGAN enhancement provider.
|
| 3 |
-
=================================
|
| 4 |
-
Uses Real-ESRGAN (https://github.com/xinntao/Real-ESRGAN) to upscale and
|
| 5 |
-
denoise scanned documents before applying classical binarisation.
|
| 6 |
-
|
| 7 |
-
This is the recommended provider for degraded photographic scans where
|
| 8 |
-
classical CV loses thin lines and text detail.
|
| 9 |
-
|
| 10 |
-
Installation
|
| 11 |
-
------------
|
| 12 |
-
pip install basicsr realesrgan torch torchvision
|
| 13 |
-
|
| 14 |
-
The model weights (~64 MB) are downloaded automatically on first use to
|
| 15 |
-
~/.cache/realesrgan/.
|
| 16 |
-
|
| 17 |
-
Why this model
|
| 18 |
-
--------------
|
| 19 |
-
Real-ESRGAN was trained on degraded images including scan noise, JPEG
|
| 20 |
-
compression, and blurring. It produces 4× upscaled output with clean edges —
|
| 21 |
-
ideal as a pre-processing step before adaptive thresholding.
|
| 22 |
-
"""
|
| 23 |
-
|
| 24 |
-
from __future__ import annotations
|
| 25 |
-
import logging
|
| 26 |
-
import os
|
| 27 |
-
from typing import Optional
|
| 28 |
-
|
| 29 |
-
import numpy as np
|
| 30 |
-
|
| 31 |
-
from .base import EnhancementProvider
|
| 32 |
-
|
| 33 |
-
logger = logging.getLogger(__name__)
|
| 34 |
-
|
| 35 |
-
_REALESRGAN_AVAILABLE = False
|
| 36 |
-
_REALESRGAN_ERROR: Optional[str] = None
|
| 37 |
-
|
| 38 |
-
try:
|
| 39 |
-
from basicsr.archs.rrdbnet_arch import RRDBNet # noqa: F401
|
| 40 |
-
from realesrgan import RealESRGANer # noqa: F401
|
| 41 |
-
_REALESRGAN_AVAILABLE = True
|
| 42 |
-
except ImportError as _e:
|
| 43 |
-
_REALESRGAN_ERROR = f"realesrgan / basicsr not installed: {_e}"
|
| 44 |
-
except Exception as _e:
|
| 45 |
-
_REALESRGAN_ERROR = f"Unexpected import error: {_e}"
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
class RealESRGANProvider(EnhancementProvider):
|
| 49 |
-
|
| 50 |
-
name = "real_esrgan"
|
| 51 |
-
display_name = "Real-ESRGAN (AI Super-Resolution)"
|
| 52 |
-
description = (
|
| 53 |
-
"4× AI upscaling + denoising using Real-ESRGAN. Recovers thin lines "
|
| 54 |
-
"and legible text from heavily degraded photographic scans. "
|
| 55 |
-
"Requires PyTorch (GPU recommended, CPU supported). "
|
| 56 |
-
"Install: pip install basicsr realesrgan torch torchvision"
|
| 57 |
-
)
|
| 58 |
-
|
| 59 |
-
# Model variant — x4plus is the best general-purpose option.
|
| 60 |
-
# Alternatives: RealESRGAN_x4plus_anime_6B (smaller), RealESRGAN_x2plus
|
| 61 |
-
_MODEL_NAME = "RealESRGAN_x4plus"
|
| 62 |
-
_SCALE = 4
|
| 63 |
-
|
| 64 |
-
def __init__(self) -> None:
|
| 65 |
-
self._upsampler = None # lazy-loaded on first enhance() call
|
| 66 |
-
|
| 67 |
-
@property
|
| 68 |
-
def is_available(self) -> bool:
|
| 69 |
-
return _REALESRGAN_AVAILABLE
|
| 70 |
-
|
| 71 |
-
@property
|
| 72 |
-
def unavailable_reason(self) -> Optional[str]:
|
| 73 |
-
return _REALESRGAN_ERROR
|
| 74 |
-
|
| 75 |
-
# Deliver at 2× the original rather than 4× — downstream OCR and line
|
| 76 |
-
# detection run 4× faster with no perceptible quality loss on the binary.
|
| 77 |
-
_OUTPUT_SCALE = 2
|
| 78 |
-
|
| 79 |
-
def enhance(self, image: np.ndarray) -> np.ndarray:
|
| 80 |
-
import cv2
|
| 81 |
-
upsampler = self._get_upsampler()
|
| 82 |
-
|
| 83 |
-
# Real-ESRGAN expects BGR uint8
|
| 84 |
-
if len(image.shape) == 2:
|
| 85 |
-
bgr = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
| 86 |
-
else:
|
| 87 |
-
bgr = image.copy()
|
| 88 |
-
|
| 89 |
-
# 4× AI super-resolution
|
| 90 |
-
output, _ = upsampler.enhance(bgr, outscale=self._SCALE)
|
| 91 |
-
|
| 92 |
-
# Downscale to _OUTPUT_SCALE× to keep OCR/line detection manageable
|
| 93 |
-
if self._OUTPUT_SCALE < self._SCALE:
|
| 94 |
-
factor = self._OUTPUT_SCALE / self._SCALE
|
| 95 |
-
h4, w4 = output.shape[:2]
|
| 96 |
-
output = cv2.resize(
|
| 97 |
-
output,
|
| 98 |
-
(int(w4 * factor), int(h4 * factor)),
|
| 99 |
-
interpolation=cv2.INTER_AREA,
|
| 100 |
-
)
|
| 101 |
-
|
| 102 |
-
logger.info("Post-AI output: %dx%d", output.shape[1], output.shape[0])
|
| 103 |
-
|
| 104 |
-
# Grayscale → contrast → binarise → cleanup
|
| 105 |
-
# (skip NL-means denoising and background removal — AI output is already clean)
|
| 106 |
-
from processors.image_enhancer import ImageEnhancer
|
| 107 |
-
gray = cv2.cvtColor(output, cv2.COLOR_BGR2GRAY)
|
| 108 |
-
enh = ImageEnhancer()
|
| 109 |
-
contrast = enh._enhance_contrast(gray)
|
| 110 |
-
binary = enh._binarize(contrast)
|
| 111 |
-
return enh._morphological_cleanup(binary)
|
| 112 |
-
|
| 113 |
-
def _get_upsampler(self):
|
| 114 |
-
"""Lazy-load the model weights on first call."""
|
| 115 |
-
if self._upsampler is not None:
|
| 116 |
-
return self._upsampler
|
| 117 |
-
|
| 118 |
-
import torch
|
| 119 |
-
from basicsr.archs.rrdbnet_arch import RRDBNet
|
| 120 |
-
from realesrgan import RealESRGANer
|
| 121 |
-
|
| 122 |
-
model = RRDBNet(
|
| 123 |
-
num_in_ch=3, num_out_ch=3,
|
| 124 |
-
num_feat=64, num_block=23, num_grow_ch=32, scale=self._SCALE,
|
| 125 |
-
)
|
| 126 |
-
# Weights are downloaded automatically by RealESRGANer on first use
|
| 127 |
-
weights_url = (
|
| 128 |
-
"https://github.com/xinntao/Real-ESRGAN/releases/download/"
|
| 129 |
-
"v0.1.0/RealESRGAN_x4plus.pth"
|
| 130 |
-
)
|
| 131 |
-
cache_dir = os.path.expanduser("~/.cache/realesrgan")
|
| 132 |
-
os.makedirs(cache_dir, exist_ok=True)
|
| 133 |
-
weights_path = os.path.join(cache_dir, f"{self._MODEL_NAME}.pth")
|
| 134 |
-
|
| 135 |
-
if torch.cuda.is_available():
|
| 136 |
-
device = torch.device("cuda")
|
| 137 |
-
elif torch.backends.mps.is_available():
|
| 138 |
-
device = torch.device("mps")
|
| 139 |
-
else:
|
| 140 |
-
device = torch.device("cpu")
|
| 141 |
-
logger.info("Real-ESRGAN loading on %s", device)
|
| 142 |
-
|
| 143 |
-
self._upsampler = RealESRGANer(
|
| 144 |
-
scale=self._SCALE,
|
| 145 |
-
model_path=weights_path if os.path.exists(weights_path) else weights_url,
|
| 146 |
-
model=model,
|
| 147 |
-
tile=512, # tile processing avoids OOM on large images
|
| 148 |
-
tile_pad=10,
|
| 149 |
-
pre_pad=0,
|
| 150 |
-
half=False, # fp16 unstable on MPS/CPU; keep fp32
|
| 151 |
-
device=device,
|
| 152 |
-
)
|
| 153 |
-
return self._upsampler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/enhancement/registry.py
DELETED
|
@@ -1,133 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Enhancement Provider Registry
|
| 3 |
-
==============================
|
| 4 |
-
Auto-discovers all EnhancementProvider subclasses in this package and
|
| 5 |
-
exposes them by name. Handles unavailable providers gracefully.
|
| 6 |
-
|
| 7 |
-
Usage
|
| 8 |
-
-----
|
| 9 |
-
from processors.enhancement.registry import registry
|
| 10 |
-
|
| 11 |
-
provider = registry.get("real_esrgan") # raises if not found/unavailable
|
| 12 |
-
provider = registry.get_or_default("real_esrgan") # falls back to classical
|
| 13 |
-
|
| 14 |
-
registry.list_all() # all providers (available + unavailable)
|
| 15 |
-
registry.list_available() # only available providers
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
import importlib
|
| 20 |
-
import logging
|
| 21 |
-
from typing import Dict, List, Optional
|
| 22 |
-
|
| 23 |
-
from .base import EnhancementProvider
|
| 24 |
-
|
| 25 |
-
logger = logging.getLogger(__name__)
|
| 26 |
-
|
| 27 |
-
# Explicit list of provider modules in this package.
|
| 28 |
-
# Add a new module name here when adding a new provider.
|
| 29 |
-
_PROVIDER_MODULES = [
|
| 30 |
-
"processors.enhancement.classical",
|
| 31 |
-
"processors.enhancement.groq_guided", # cloud — needs GROQ_API_KEY
|
| 32 |
-
"processors.enhancement.huggingface_api", # cloud — free HF token
|
| 33 |
-
"processors.enhancement.real_esrgan", # local — needs GPU + torch
|
| 34 |
-
"processors.enhancement.huggingface", # local — needs torch
|
| 35 |
-
]
|
| 36 |
-
|
| 37 |
-
_DEFAULT_PROVIDER = "classical"
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
class EnhancementRegistry:
|
| 41 |
-
"""Singleton registry of all enhancement providers."""
|
| 42 |
-
|
| 43 |
-
def __init__(self) -> None:
|
| 44 |
-
self._providers: Dict[str, EnhancementProvider] = {}
|
| 45 |
-
self._load_all()
|
| 46 |
-
|
| 47 |
-
def _load_all(self) -> None:
|
| 48 |
-
for module_path in _PROVIDER_MODULES:
|
| 49 |
-
mod = self._import_module(module_path)
|
| 50 |
-
if mod is not None:
|
| 51 |
-
self._register_providers_from(mod)
|
| 52 |
-
|
| 53 |
-
def _import_module(self, module_path: str):
|
| 54 |
-
try:
|
| 55 |
-
return importlib.import_module(module_path)
|
| 56 |
-
except ImportError as exc:
|
| 57 |
-
logger.debug("Skipping provider module %s: %s", module_path, exc)
|
| 58 |
-
return None
|
| 59 |
-
|
| 60 |
-
def _register_providers_from(self, mod) -> None:
|
| 61 |
-
for attr_name in dir(mod):
|
| 62 |
-
cls = getattr(mod, attr_name)
|
| 63 |
-
if self._is_provider_class(cls):
|
| 64 |
-
self._instantiate_and_register(cls, attr_name)
|
| 65 |
-
|
| 66 |
-
@staticmethod
|
| 67 |
-
def _is_provider_class(obj) -> bool:
|
| 68 |
-
return (
|
| 69 |
-
isinstance(obj, type)
|
| 70 |
-
and issubclass(obj, EnhancementProvider)
|
| 71 |
-
and obj is not EnhancementProvider
|
| 72 |
-
and bool(obj.name)
|
| 73 |
-
)
|
| 74 |
-
|
| 75 |
-
def _instantiate_and_register(self, cls, attr_name: str) -> None:
|
| 76 |
-
try:
|
| 77 |
-
instance = cls()
|
| 78 |
-
self._providers[instance.name] = instance
|
| 79 |
-
status = "available" if instance.is_available else "unavailable"
|
| 80 |
-
logger.info("Enhancement provider loaded: %s (%s)", instance.name, status)
|
| 81 |
-
except Exception as exc:
|
| 82 |
-
logger.warning("Failed to instantiate provider %s: %s", attr_name, exc)
|
| 83 |
-
|
| 84 |
-
# ------------------------------------------------------------------ #
|
| 85 |
-
# Public API #
|
| 86 |
-
# ------------------------------------------------------------------ #
|
| 87 |
-
|
| 88 |
-
def get(self, name: str) -> EnhancementProvider:
|
| 89 |
-
"""
|
| 90 |
-
Return the provider for `name`.
|
| 91 |
-
Raises ValueError if unknown or unavailable.
|
| 92 |
-
"""
|
| 93 |
-
provider = self._providers.get(name)
|
| 94 |
-
if provider is None:
|
| 95 |
-
available = ", ".join(self._providers.keys())
|
| 96 |
-
raise ValueError(
|
| 97 |
-
f"Unknown enhancement model '{name}'. "
|
| 98 |
-
f"Available: {available}"
|
| 99 |
-
)
|
| 100 |
-
if not provider.is_available:
|
| 101 |
-
raise ValueError(
|
| 102 |
-
f"Enhancement model '{name}' is not available: "
|
| 103 |
-
f"{provider.unavailable_reason}"
|
| 104 |
-
)
|
| 105 |
-
return provider
|
| 106 |
-
|
| 107 |
-
def get_or_default(self, name: Optional[str]) -> EnhancementProvider:
|
| 108 |
-
"""
|
| 109 |
-
Return the provider for `name`, falling back to the default classical
|
| 110 |
-
provider if `name` is None, unknown, or unavailable.
|
| 111 |
-
"""
|
| 112 |
-
if name is None:
|
| 113 |
-
return self.get(_DEFAULT_PROVIDER)
|
| 114 |
-
try:
|
| 115 |
-
return self.get(name)
|
| 116 |
-
except ValueError as exc:
|
| 117 |
-
logger.warning("%s — falling back to classical provider", exc)
|
| 118 |
-
return self.get(_DEFAULT_PROVIDER)
|
| 119 |
-
|
| 120 |
-
def list_all(self) -> List[dict]:
|
| 121 |
-
"""Return info dicts for every registered provider."""
|
| 122 |
-
return [p.info() for p in self._providers.values()]
|
| 123 |
-
|
| 124 |
-
def list_available(self) -> List[dict]:
|
| 125 |
-
"""Return info dicts for available providers only."""
|
| 126 |
-
return [p.info() for p in self._providers.values() if p.is_available]
|
| 127 |
-
|
| 128 |
-
def names(self) -> List[str]:
|
| 129 |
-
return list(self._providers.keys())
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
# Module-level singleton — import this everywhere
|
| 133 |
-
registry = EnhancementRegistry()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/ocr_classification.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
OCR Classification
|
| 3 |
-
===================
|
| 4 |
-
Shared text-classification rules used by every OCR provider (Tesseract,
|
| 5 |
-
PaddleOCR, ...) so a phrase like "3,640 x 3,640" or "SCALE" is bucketed the
|
| 6 |
-
same way regardless of which engine recognised it.
|
| 7 |
-
|
| 8 |
-
Classification buckets:
|
| 9 |
-
- dimensions : numeric strings WITH a recognised unit suffix (mm/cm/m/ft/in/'/")
|
| 10 |
-
or a "WIDTH x HEIGHT" pair (with or without a unit suffix)
|
| 11 |
-
- room_names : multi-character capitalised phrases (title-block keywords excluded)
|
| 12 |
-
- numbers : bare numeric tokens (no unit suffix)
|
| 13 |
-
- labels : everything else with sufficient confidence
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
import re
|
| 17 |
-
from typing import Any, Dict, List
|
| 18 |
-
|
| 19 |
-
# Patterns for classification.
|
| 20 |
-
# Digits may use comma thousands-separators (e.g. "3,640"), common in
|
| 21 |
-
# architectural dimension callouts.
|
| 22 |
-
_NUM = r"\d+(?:,\d{3})*(?:\.\d+)?"
|
| 23 |
-
_DIM_RE = re.compile(rf"^{_NUM}\s*(mm|cm|m|ft|in|'|\")$", re.IGNORECASE)
|
| 24 |
-
_DIM_PAIR_RE = re.compile(
|
| 25 |
-
rf"^{_NUM}\s*[x×]\s*{_NUM}\s*(mm|cm|m|ft|in|'|\")?$", re.IGNORECASE
|
| 26 |
-
)
|
| 27 |
-
_NUM_RE = re.compile(rf"^{_NUM}$")
|
| 28 |
-
|
| 29 |
-
# Title-block / drawing-metadata words that are capitalised (and would
|
| 30 |
-
# otherwise pass the room-name heuristic) but are not room names.
|
| 31 |
-
_TITLE_BLOCK_WORDS = {
|
| 32 |
-
"scale", "floor", "plan", "level", "section", "elevation", "detail",
|
| 33 |
-
"site", "drawn", "checked", "approved", "date", "rev", "revision",
|
| 34 |
-
"sheet", "north", "title", "project", "drawing", "no", "nts", "n.t.s",
|
| 35 |
-
"scale:", "client", "architect", "job",
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def classify(elements: List[Dict[str, Any]]) -> Dict[str, List]:
|
| 40 |
-
classified: Dict[str, List] = {
|
| 41 |
-
"dimensions": [],
|
| 42 |
-
"room_names": [],
|
| 43 |
-
"numbers": [],
|
| 44 |
-
"labels": [],
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
for elem in elements:
|
| 48 |
-
text = elem["text"]
|
| 49 |
-
|
| 50 |
-
if _DIM_PAIR_RE.match(text) or _DIM_RE.match(text):
|
| 51 |
-
classified["dimensions"].append(elem)
|
| 52 |
-
elif _NUM_RE.match(text):
|
| 53 |
-
classified["numbers"].append(elem)
|
| 54 |
-
elif is_title_block(text):
|
| 55 |
-
classified["labels"].append(elem)
|
| 56 |
-
elif len(text) > 2 and text[0].isupper():
|
| 57 |
-
classified["room_names"].append(elem)
|
| 58 |
-
else:
|
| 59 |
-
classified["labels"].append(elem)
|
| 60 |
-
|
| 61 |
-
return classified
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def is_title_block(text: str) -> bool:
|
| 65 |
-
"""True if any word in the phrase is a drawing-metadata keyword."""
|
| 66 |
-
words = re.findall(r"[A-Za-z.:]+", text.lower())
|
| 67 |
-
return any(w in _TITLE_BLOCK_WORDS for w in words)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def empty_result(reason: str) -> Dict[str, Any]:
|
| 71 |
-
return {
|
| 72 |
-
"full_text": "",
|
| 73 |
-
"element_count": 0,
|
| 74 |
-
"elements": [],
|
| 75 |
-
"classified": {
|
| 76 |
-
"dimensions": [],
|
| 77 |
-
"room_names": [],
|
| 78 |
-
"numbers": [],
|
| 79 |
-
"labels": [],
|
| 80 |
-
},
|
| 81 |
-
"error": reason,
|
| 82 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processors/paddleocr_processor.py
DELETED
|
@@ -1,157 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
PaddleOCR Processor
|
| 3 |
-
===================
|
| 4 |
-
Alternative to Tesseract (see ocr_processor.py) backed by PaddleOCR
|
| 5 |
-
(Apache 2.0, https://github.com/PaddlePaddle/PaddleOCR).
|
| 6 |
-
|
| 7 |
-
Why this is a separate, simpler class rather than another Tesseract config:
|
| 8 |
-
PaddleOCR's detector locates text at the phrase/line level directly, so none
|
| 9 |
-
of ocr_processor.py's word->phrase grouping, gap-splitting, or tick-mark
|
| 10 |
-
height filtering is needed here — those all exist specifically to work
|
| 11 |
-
around Tesseract returning individual words with no semantic grouping.
|
| 12 |
-
Classification (dimensions/room_names/numbers/labels) is shared via
|
| 13 |
-
ocr_classification.py so both engines bucket text identically.
|
| 14 |
-
|
| 15 |
-
Status: NOT YET BENCHMARKED. Model weights could not be downloaded in the
|
| 16 |
-
sandboxed dev environment this was written in — see CLAUDE.md for why
|
| 17 |
-
(corporate TLS-inspecting proxy whose root CA isn't in Python's default
|
| 18 |
-
trust store). The API calls below are verified against the installed
|
| 19 |
-
paddleocr==3.7.0 / paddlex package source directly (constructor params,
|
| 20 |
-
and the exact result schema: `rec_texts`/`rec_scores`/`rec_boxes`, with
|
| 21 |
-
`rec_boxes[i] == [left, top, right, bottom]` — see
|
| 22 |
-
venv/lib/python3.12/site-packages/paddlex/inference/pipelines/ocr/pipeline.py
|
| 23 |
-
and .../components/common/convert_points_and_boxes.py), but real accuracy
|
| 24 |
-
on this project's sample images has not been measured. Run
|
| 25 |
-
`benchmark/run_benchmark.py` before relying on this over Tesseract.
|
| 26 |
-
"""
|
| 27 |
-
|
| 28 |
-
from __future__ import annotations
|
| 29 |
-
import logging
|
| 30 |
-
import os
|
| 31 |
-
from typing import Any, Dict, List, Optional
|
| 32 |
-
|
| 33 |
-
import numpy as np
|
| 34 |
-
|
| 35 |
-
from .ocr_classification import classify, empty_result
|
| 36 |
-
|
| 37 |
-
logger = logging.getLogger(__name__)
|
| 38 |
-
|
| 39 |
-
# PP-OCRv6's detection/recognition models crash under paddlex's default CPU
|
| 40 |
-
# MKLDNN/PIR execution path on this Windows/CPU combo:
|
| 41 |
-
# NotImplementedError: (Unimplemented) ConvertPirAttribute2RuntimeAttribute
|
| 42 |
-
# not support [pir::ArrayAttribute<pir::DoubleAttribute>]
|
| 43 |
-
# Neither model is in paddlex's MKLDNN_BLOCKLIST (an upstream gap), so the
|
| 44 |
-
# workaround is disabling MKLDNN-by-default before paddlex's flags module
|
| 45 |
-
# is imported. Must happen before `import paddleocr`, not after.
|
| 46 |
-
os.environ.setdefault("PADDLE_PDX_ENABLE_MKLDNN_BYDEFAULT", "False")
|
| 47 |
-
|
| 48 |
-
try:
|
| 49 |
-
from paddleocr import PaddleOCR as _PaddleOCREngine
|
| 50 |
-
_PADDLEOCR_AVAILABLE = True
|
| 51 |
-
_PADDLEOCR_ERROR: Optional[str] = None
|
| 52 |
-
except ImportError as _e:
|
| 53 |
-
_PADDLEOCR_AVAILABLE = False
|
| 54 |
-
_PADDLEOCR_ERROR = f"paddleocr / paddlepaddle not installed: {_e}"
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
class PaddleOCRProcessor:
|
| 58 |
-
"""
|
| 59 |
-
Drop-in alternative to OCRProcessor — same extract_text() contract
|
| 60 |
-
(full_text / element_count / elements / classified), so main.py can
|
| 61 |
-
swap between engines without any other code changing.
|
| 62 |
-
"""
|
| 63 |
-
|
| 64 |
-
def __init__(self, lang: str = "en") -> None:
|
| 65 |
-
# PaddleOCR loads one recognition model per language (unlike
|
| 66 |
-
# Tesseract's combined "jpn+eng"); "en" covers this project's
|
| 67 |
-
# sample images, pass lang="japan" for Japanese-only drawings.
|
| 68 |
-
self._lang = lang
|
| 69 |
-
self._engine = None # lazy-loaded on first use — downloads weights
|
| 70 |
-
|
| 71 |
-
@property
|
| 72 |
-
def is_available(self) -> bool:
|
| 73 |
-
return _PADDLEOCR_AVAILABLE
|
| 74 |
-
|
| 75 |
-
@property
|
| 76 |
-
def unavailable_reason(self) -> Optional[str]:
|
| 77 |
-
return _PADDLEOCR_ERROR
|
| 78 |
-
|
| 79 |
-
def extract_text(self, image: np.ndarray) -> Dict[str, Any]:
|
| 80 |
-
"""
|
| 81 |
-
Parameters
|
| 82 |
-
----------
|
| 83 |
-
image : enhanced binary or grayscale (or BGR) image
|
| 84 |
-
|
| 85 |
-
Returns
|
| 86 |
-
-------
|
| 87 |
-
Same schema as OCRProcessor.extract_text() — see that module's
|
| 88 |
-
docstring.
|
| 89 |
-
"""
|
| 90 |
-
if not _PADDLEOCR_AVAILABLE:
|
| 91 |
-
logger.warning("paddleocr not installed – OCR skipped")
|
| 92 |
-
return empty_result("paddleocr not installed")
|
| 93 |
-
|
| 94 |
-
try:
|
| 95 |
-
engine = self._get_engine()
|
| 96 |
-
results = engine.predict(image)
|
| 97 |
-
except Exception as exc:
|
| 98 |
-
logger.exception("PaddleOCR error")
|
| 99 |
-
return empty_result(str(exc))
|
| 100 |
-
|
| 101 |
-
if not results:
|
| 102 |
-
return empty_result("no text regions detected")
|
| 103 |
-
|
| 104 |
-
result = results[0]
|
| 105 |
-
elements = self._to_elements(
|
| 106 |
-
result.get("rec_texts", []),
|
| 107 |
-
result.get("rec_scores", []),
|
| 108 |
-
result.get("rec_boxes", []),
|
| 109 |
-
)
|
| 110 |
-
classified = classify(elements)
|
| 111 |
-
|
| 112 |
-
return {
|
| 113 |
-
"full_text": "\n".join(e["text"] for e in elements),
|
| 114 |
-
"element_count": len(elements),
|
| 115 |
-
"elements": elements,
|
| 116 |
-
"classified": classified,
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
# ------------------------------------------------------------------ #
|
| 120 |
-
# Private helpers #
|
| 121 |
-
# ------------------------------------------------------------------ #
|
| 122 |
-
|
| 123 |
-
def _get_engine(self):
|
| 124 |
-
if self._engine is not None:
|
| 125 |
-
return self._engine
|
| 126 |
-
logger.info("Loading PaddleOCR engine (lang=%s) — downloads weights on first use", self._lang)
|
| 127 |
-
self._engine = _PaddleOCREngine(
|
| 128 |
-
lang=self._lang,
|
| 129 |
-
# Our own enhancement pipeline already deskews/upscales the
|
| 130 |
-
# image, so skip PaddleOCR's own document-orientation
|
| 131 |
-
# classification and unwarping sub-pipelines — one less model
|
| 132 |
-
# to download and one less inference pass per image.
|
| 133 |
-
use_doc_orientation_classify=False,
|
| 134 |
-
use_doc_unwarping=False,
|
| 135 |
-
use_textline_orientation=False,
|
| 136 |
-
)
|
| 137 |
-
return self._engine
|
| 138 |
-
|
| 139 |
-
@staticmethod
|
| 140 |
-
def _to_elements(
|
| 141 |
-
texts: List[str], scores: List[float], boxes: Any
|
| 142 |
-
) -> List[Dict[str, Any]]:
|
| 143 |
-
elements: List[Dict[str, Any]] = []
|
| 144 |
-
for text, score, box in zip(texts, scores, boxes):
|
| 145 |
-
text = str(text).strip()
|
| 146 |
-
if not text:
|
| 147 |
-
continue
|
| 148 |
-
x1, y1, x2, y2 = (float(v) for v in box)
|
| 149 |
-
elements.append({
|
| 150 |
-
"text": text,
|
| 151 |
-
"confidence": round(float(score) * 100), # match Tesseract's 0-100 scale
|
| 152 |
-
"x": int(round(x1)),
|
| 153 |
-
"y": int(round(y1)),
|
| 154 |
-
"width": int(round(x2 - x1)),
|
| 155 |
-
"height": int(round(y2 - y1)),
|
| 156 |
-
})
|
| 157 |
-
return elements
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.cloud.txt
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
# Cloud deployment — excludes torch/Real-ESRGAN (too large for serverless)
|
| 2 |
-
# Real-ESRGAN is available when running locally only.
|
| 3 |
-
fastapi==0.115.0
|
| 4 |
-
uvicorn[standard]==0.30.6
|
| 5 |
-
python-multipart==0.0.9
|
| 6 |
-
opencv-python-headless==4.10.0.84
|
| 7 |
-
numpy>=1.26.4
|
| 8 |
-
Pillow==10.4.0
|
| 9 |
-
pytesseract==0.3.13
|
| 10 |
-
anthropic==0.34.2
|
| 11 |
-
openai>=1.0.0
|
| 12 |
-
google-genai>=1.0.0
|
| 13 |
-
groq>=0.9.0
|
| 14 |
-
python-dotenv==1.0.1
|
| 15 |
-
svgwrite==1.4.3
|
| 16 |
-
scipy==1.13.1
|
| 17 |
-
huggingface_hub>=1.21.0
|
| 18 |
-
requests>=2.31.0
|
| 19 |
-
scikit-image>=0.21.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/__init__.py
DELETED
|
File without changes
|
tests/conftest.py
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Shared pytest fixtures for backend tests.
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import io
|
| 6 |
-
import sys
|
| 7 |
-
import os
|
| 8 |
-
|
| 9 |
-
import cv2
|
| 10 |
-
import numpy as np
|
| 11 |
-
import pytest
|
| 12 |
-
|
| 13 |
-
# Make sure the backend package is importable
|
| 14 |
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
@pytest.fixture
|
| 18 |
-
def synthetic_drawing() -> np.ndarray:
|
| 19 |
-
"""800×600 grayscale floor plan with walls, interior lines and text."""
|
| 20 |
-
img = np.ones((600, 800), dtype=np.uint8) * 240
|
| 21 |
-
cv2.rectangle(img, (50, 50), (750, 550), 0, 3)
|
| 22 |
-
cv2.line(img, (400, 50), (400, 350), 0, 2)
|
| 23 |
-
cv2.line(img, (50, 300), (400, 300), 0, 2)
|
| 24 |
-
font = cv2.FONT_HERSHEY_SIMPLEX
|
| 25 |
-
cv2.putText(img, "3640", (180, 285), font, 1.2, 0, 2)
|
| 26 |
-
cv2.putText(img, "Living Room", (100, 430), font, 1.0, 0, 2)
|
| 27 |
-
cv2.putText(img, "Bedroom", (470, 220), font, 1.0, 0, 2)
|
| 28 |
-
return img
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
@pytest.fixture
|
| 32 |
-
def synthetic_drawing_bgr(synthetic_drawing) -> np.ndarray:
|
| 33 |
-
return cv2.cvtColor(synthetic_drawing, cv2.COLOR_GRAY2BGR)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
@pytest.fixture
|
| 37 |
-
def drawing_bytes(synthetic_drawing) -> bytes:
|
| 38 |
-
"""PNG-encoded bytes of the synthetic drawing."""
|
| 39 |
-
_, buf = cv2.imencode(".png", synthetic_drawing)
|
| 40 |
-
return buf.tobytes()
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
@pytest.fixture
|
| 44 |
-
def drawing_file(drawing_bytes):
|
| 45 |
-
"""File-like object for use with FastAPI TestClient."""
|
| 46 |
-
return ("drawing.png", io.BytesIO(drawing_bytes), "image/png")
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
@pytest.fixture
|
| 50 |
-
def client():
|
| 51 |
-
from fastapi.testclient import TestClient
|
| 52 |
-
from main import app
|
| 53 |
-
return TestClient(app)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_api.py
DELETED
|
@@ -1,122 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Integration tests for the FastAPI endpoints using TestClient.
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import io
|
| 6 |
-
import pytest
|
| 7 |
-
from fastapi.testclient import TestClient
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
class TestHealthEndpoint:
|
| 11 |
-
def test_returns_200(self, client):
|
| 12 |
-
resp = client.get("/health")
|
| 13 |
-
assert resp.status_code == 200
|
| 14 |
-
|
| 15 |
-
def test_status_is_healthy(self, client):
|
| 16 |
-
data = client.get("/health").json()
|
| 17 |
-
assert data["status"] == "healthy"
|
| 18 |
-
|
| 19 |
-
def test_all_processors_listed(self, client):
|
| 20 |
-
data = client.get("/health").json()
|
| 21 |
-
processors = data["processors"]
|
| 22 |
-
for name in ("image_enhancer", "line_extractor", "ocr_processor", "svg_generator"):
|
| 23 |
-
assert name in processors
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
class TestInfoEndpoint:
|
| 27 |
-
def test_returns_200(self, client):
|
| 28 |
-
assert client.get("/api/info").status_code == 200
|
| 29 |
-
|
| 30 |
-
def test_returns_processing_options(self, client):
|
| 31 |
-
data = client.get("/api/info").json()
|
| 32 |
-
assert "processing_options" in data
|
| 33 |
-
|
| 34 |
-
def test_all_options_listed(self, client):
|
| 35 |
-
opts = client.get("/api/info").json()["processing_options"]
|
| 36 |
-
for key in ("enhance", "extract_lines", "run_ocr", "generate_svg", "run_ai"):
|
| 37 |
-
assert key in opts
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
class TestProcessEndpoint:
|
| 41 |
-
def test_returns_200_with_valid_image(self, client, drawing_file):
|
| 42 |
-
resp = client.post("/api/process", files={"file": drawing_file})
|
| 43 |
-
assert resp.status_code == 200
|
| 44 |
-
|
| 45 |
-
def test_response_has_required_fields(self, client, drawing_file):
|
| 46 |
-
data = client.post("/api/process", files={"file": drawing_file}).json()
|
| 47 |
-
for field in ("filename", "original_size", "processing_steps", "processing_time_s"):
|
| 48 |
-
assert field in data
|
| 49 |
-
|
| 50 |
-
def test_enhance_step_runs_by_default(self, client, drawing_file):
|
| 51 |
-
data = client.post("/api/process", files={"file": drawing_file}).json()
|
| 52 |
-
assert "image_enhancement" in data["processing_steps"]
|
| 53 |
-
assert "enhanced_image" in data
|
| 54 |
-
|
| 55 |
-
def test_line_extraction_step_runs_by_default(self, client, drawing_file):
|
| 56 |
-
data = client.post("/api/process", files={"file": drawing_file}).json()
|
| 57 |
-
assert "line_extraction" in data["processing_steps"]
|
| 58 |
-
assert "detected_lines" in data
|
| 59 |
-
|
| 60 |
-
def test_ocr_step_runs_by_default(self, client, drawing_file):
|
| 61 |
-
data = client.post("/api/process", files={"file": drawing_file}).json()
|
| 62 |
-
assert "ocr" in data["processing_steps"]
|
| 63 |
-
assert "ocr_results" in data
|
| 64 |
-
|
| 65 |
-
def test_svg_not_generated_by_default(self, client, drawing_file):
|
| 66 |
-
data = client.post("/api/process", files={"file": drawing_file}).json()
|
| 67 |
-
assert "svg_generation" not in data["processing_steps"]
|
| 68 |
-
|
| 69 |
-
def test_svg_generated_when_requested(self, client, drawing_file):
|
| 70 |
-
resp = client.post(
|
| 71 |
-
"/api/process",
|
| 72 |
-
files={"file": drawing_file},
|
| 73 |
-
data={"generate_svg": "true"},
|
| 74 |
-
)
|
| 75 |
-
data = resp.json()
|
| 76 |
-
assert "svg_generation" in data["processing_steps"]
|
| 77 |
-
assert "svg_output" in data
|
| 78 |
-
|
| 79 |
-
def test_enhance_only_skips_other_steps(self, client, drawing_file):
|
| 80 |
-
resp = client.post(
|
| 81 |
-
"/api/process",
|
| 82 |
-
files={"file": drawing_file},
|
| 83 |
-
data={"enhance": "true", "extract_lines": "false", "run_ocr": "false"},
|
| 84 |
-
)
|
| 85 |
-
data = resp.json()
|
| 86 |
-
assert "image_enhancement" in data["processing_steps"]
|
| 87 |
-
assert "line_extraction" not in data["processing_steps"]
|
| 88 |
-
assert "ocr" not in data["processing_steps"]
|
| 89 |
-
|
| 90 |
-
def test_detected_lines_is_integer(self, client, drawing_file):
|
| 91 |
-
data = client.post("/api/process", files={"file": drawing_file}).json()
|
| 92 |
-
assert isinstance(data.get("detected_lines"), int)
|
| 93 |
-
|
| 94 |
-
def test_processing_time_is_float(self, client, drawing_file):
|
| 95 |
-
data = client.post("/api/process", files={"file": drawing_file}).json()
|
| 96 |
-
assert isinstance(data["processing_time_s"], float)
|
| 97 |
-
assert data["processing_time_s"] > 0
|
| 98 |
-
|
| 99 |
-
def test_invalid_file_returns_400(self, client):
|
| 100 |
-
fake_file = ("bad.png", io.BytesIO(b"not an image"), "image/png")
|
| 101 |
-
resp = client.post("/api/process", files={"file": fake_file})
|
| 102 |
-
assert resp.status_code == 400
|
| 103 |
-
|
| 104 |
-
def test_original_size_matches_image(self, client, drawing_bytes):
|
| 105 |
-
# Our synthetic image is 800×600
|
| 106 |
-
file = ("drawing.png", io.BytesIO(drawing_bytes), "image/png")
|
| 107 |
-
data = client.post("/api/process", files={"file": file}).json()
|
| 108 |
-
size = data["original_size"]
|
| 109 |
-
assert size["width"] == 800
|
| 110 |
-
assert size["height"] == 600
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
class TestAnalyzeEndpoint:
|
| 114 |
-
def test_returns_501_without_api_key(self, client, drawing_file, monkeypatch):
|
| 115 |
-
monkeypatch.setenv("ANTHROPIC_API_KEY", "")
|
| 116 |
-
resp = client.post("/api/analyze", files={"file": drawing_file})
|
| 117 |
-
assert resp.status_code == 501
|
| 118 |
-
|
| 119 |
-
def test_error_message_mentions_api_key(self, client, drawing_file, monkeypatch):
|
| 120 |
-
monkeypatch.setenv("ANTHROPIC_API_KEY", "")
|
| 121 |
-
data = client.post("/api/analyze", files={"file": drawing_file}).json()
|
| 122 |
-
assert "ANTHROPIC_API_KEY" in data["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_image_enhancer.py
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unit tests for ImageEnhancer.
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import cv2
|
| 6 |
-
import numpy as np
|
| 7 |
-
import pytest
|
| 8 |
-
|
| 9 |
-
from processors.image_enhancer import ImageEnhancer
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@pytest.fixture
|
| 13 |
-
def enhancer():
|
| 14 |
-
return ImageEnhancer()
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
class TestEnhance:
|
| 18 |
-
def test_output_same_spatial_size(self, enhancer, synthetic_drawing):
|
| 19 |
-
result = enhancer.enhance(synthetic_drawing)
|
| 20 |
-
assert result.shape[:2] == synthetic_drawing.shape[:2]
|
| 21 |
-
|
| 22 |
-
def test_output_is_binary(self, enhancer, synthetic_drawing):
|
| 23 |
-
result = enhancer.enhance(synthetic_drawing)
|
| 24 |
-
unique = set(np.unique(result).tolist())
|
| 25 |
-
assert unique.issubset({0, 255}), f"Expected only 0/255, got {unique}"
|
| 26 |
-
|
| 27 |
-
def test_accepts_grayscale_input(self, enhancer, synthetic_drawing):
|
| 28 |
-
assert len(synthetic_drawing.shape) == 2
|
| 29 |
-
result = enhancer.enhance(synthetic_drawing)
|
| 30 |
-
assert result is not None
|
| 31 |
-
|
| 32 |
-
def test_accepts_bgr_input(self, enhancer, synthetic_drawing_bgr):
|
| 33 |
-
assert synthetic_drawing_bgr.shape[2] == 3
|
| 34 |
-
result = enhancer.enhance(synthetic_drawing_bgr)
|
| 35 |
-
assert result is not None
|
| 36 |
-
assert len(result.shape) == 2 # always returns grayscale
|
| 37 |
-
|
| 38 |
-
def test_output_dtype_is_uint8(self, enhancer, synthetic_drawing):
|
| 39 |
-
result = enhancer.enhance(synthetic_drawing)
|
| 40 |
-
assert result.dtype == np.uint8
|
| 41 |
-
|
| 42 |
-
def test_enhancer_reduces_noise(self, enhancer):
|
| 43 |
-
noisy = np.random.randint(0, 256, (200, 200), dtype=np.uint8)
|
| 44 |
-
result = enhancer.enhance(noisy)
|
| 45 |
-
# Binary output should have far fewer unique grey levels than random noise
|
| 46 |
-
assert len(np.unique(result)) <= 2
|
| 47 |
-
|
| 48 |
-
def test_enhance_grayscale_method_not_binary(self, enhancer, synthetic_drawing):
|
| 49 |
-
result = enhancer.enhance_grayscale(synthetic_drawing)
|
| 50 |
-
unique_values = np.unique(result)
|
| 51 |
-
# Grayscale output should have more than 2 grey levels
|
| 52 |
-
assert len(unique_values) > 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_line_extractor.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unit tests for LineExtractor.
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import cv2
|
| 6 |
-
import numpy as np
|
| 7 |
-
import pytest
|
| 8 |
-
|
| 9 |
-
from processors.line_extractor import LineExtractor
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@pytest.fixture
|
| 13 |
-
def extractor():
|
| 14 |
-
return LineExtractor()
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
@pytest.fixture
|
| 18 |
-
def clear_line_drawing():
|
| 19 |
-
"""White canvas with a few crisp black lines — guaranteed to be detected."""
|
| 20 |
-
img = np.ones((400, 600), dtype=np.uint8) * 255
|
| 21 |
-
cv2.line(img, (50, 200), (550, 200), 0, 3) # horizontal
|
| 22 |
-
cv2.line(img, (300, 50), (300, 350), 0, 3) # vertical
|
| 23 |
-
cv2.line(img, (50, 50), (550, 350), 0, 3) # diagonal
|
| 24 |
-
return img
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
class TestExtract:
|
| 28 |
-
def test_returns_tuple(self, extractor, synthetic_drawing):
|
| 29 |
-
result = extractor.extract(synthetic_drawing)
|
| 30 |
-
assert isinstance(result, tuple) and len(result) == 2
|
| 31 |
-
|
| 32 |
-
def test_visualisation_is_bgr(self, extractor, clear_line_drawing):
|
| 33 |
-
vis, _ = extractor.extract(clear_line_drawing)
|
| 34 |
-
assert len(vis.shape) == 3 and vis.shape[2] == 3
|
| 35 |
-
|
| 36 |
-
def test_visualisation_same_size(self, extractor, synthetic_drawing):
|
| 37 |
-
vis, _ = extractor.extract(synthetic_drawing)
|
| 38 |
-
assert vis.shape[:2] == synthetic_drawing.shape[:2]
|
| 39 |
-
|
| 40 |
-
def test_detects_lines_in_drawing(self, extractor, clear_line_drawing):
|
| 41 |
-
_, lines = extractor.extract(clear_line_drawing, hough_threshold=30)
|
| 42 |
-
assert len(lines) >= 3, f"Expected ≥3 lines, got {len(lines)}"
|
| 43 |
-
|
| 44 |
-
def test_blank_image_returns_no_lines(self, extractor):
|
| 45 |
-
blank = np.ones((300, 400), dtype=np.uint8) * 255
|
| 46 |
-
_, lines = extractor.extract(blank)
|
| 47 |
-
assert lines == []
|
| 48 |
-
|
| 49 |
-
def test_line_dict_has_required_keys(self, extractor, clear_line_drawing):
|
| 50 |
-
_, lines = extractor.extract(clear_line_drawing, hough_threshold=30)
|
| 51 |
-
assert lines, "No lines detected — cannot check keys"
|
| 52 |
-
for line in lines:
|
| 53 |
-
for key in ("x1", "y1", "x2", "y2", "angle", "length"):
|
| 54 |
-
assert key in line, f"Missing key '{key}' in line dict"
|
| 55 |
-
|
| 56 |
-
def test_line_length_is_positive(self, extractor, clear_line_drawing):
|
| 57 |
-
_, lines = extractor.extract(clear_line_drawing, hough_threshold=30)
|
| 58 |
-
for line in lines:
|
| 59 |
-
assert line["length"] > 0
|
| 60 |
-
|
| 61 |
-
def test_accepts_bgr_input(self, extractor, synthetic_drawing_bgr):
|
| 62 |
-
vis, lines = extractor.extract(synthetic_drawing_bgr)
|
| 63 |
-
assert vis is not None
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
class TestClassifyAngle:
|
| 67 |
-
def test_horizontal(self, extractor):
|
| 68 |
-
assert extractor._classify_angle(0) == "horizontal"
|
| 69 |
-
assert extractor._classify_angle(10) == "horizontal"
|
| 70 |
-
assert extractor._classify_angle(-10) == "horizontal"
|
| 71 |
-
assert extractor._classify_angle(170) == "horizontal"
|
| 72 |
-
|
| 73 |
-
def test_vertical(self, extractor):
|
| 74 |
-
assert extractor._classify_angle(90) == "vertical"
|
| 75 |
-
assert extractor._classify_angle(80) == "vertical"
|
| 76 |
-
assert extractor._classify_angle(100) == "vertical"
|
| 77 |
-
|
| 78 |
-
def test_diagonal(self, extractor):
|
| 79 |
-
assert extractor._classify_angle(45) == "diagonal"
|
| 80 |
-
assert extractor._classify_angle(-45) == "diagonal"
|
| 81 |
-
assert extractor._classify_angle(135) == "diagonal"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_ocr_processor.py
DELETED
|
@@ -1,106 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unit tests for OCRProcessor.
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import cv2
|
| 6 |
-
import numpy as np
|
| 7 |
-
import pytest
|
| 8 |
-
|
| 9 |
-
from processors.ocr_processor import OCRProcessor
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@pytest.fixture
|
| 13 |
-
def processor():
|
| 14 |
-
return OCRProcessor()
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
@pytest.fixture
|
| 18 |
-
def text_drawing():
|
| 19 |
-
"""Large, clear image with well-spaced text for reliable OCR."""
|
| 20 |
-
img = np.ones((900, 1200), dtype=np.uint8) * 255
|
| 21 |
-
font = cv2.FONT_HERSHEY_SIMPLEX
|
| 22 |
-
cv2.putText(img, "Living Room", (100, 200), font, 2.0, 0, 4)
|
| 23 |
-
cv2.putText(img, "3640", (100, 450), font, 2.0, 0, 4)
|
| 24 |
-
cv2.putText(img, "Bedroom", (100, 700), font, 2.0, 0, 4)
|
| 25 |
-
return img
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class TestExtractText:
|
| 29 |
-
def test_returns_dict_with_expected_keys(self, processor, synthetic_drawing):
|
| 30 |
-
result = processor.extract_text(synthetic_drawing)
|
| 31 |
-
for key in ("full_text", "element_count", "elements", "classified"):
|
| 32 |
-
assert key in result, f"Missing key: {key}"
|
| 33 |
-
|
| 34 |
-
def test_classified_has_correct_categories(self, processor, synthetic_drawing):
|
| 35 |
-
result = processor.extract_text(synthetic_drawing)
|
| 36 |
-
for cat in ("dimensions", "room_names", "numbers", "labels"):
|
| 37 |
-
assert cat in result["classified"]
|
| 38 |
-
|
| 39 |
-
def test_element_count_matches_elements_list(self, processor, text_drawing):
|
| 40 |
-
result = processor.extract_text(text_drawing)
|
| 41 |
-
assert result["element_count"] == len(result["elements"])
|
| 42 |
-
|
| 43 |
-
def test_each_element_has_required_fields(self, processor, text_drawing):
|
| 44 |
-
result = processor.extract_text(text_drawing)
|
| 45 |
-
for el in result["elements"]:
|
| 46 |
-
for field in ("text", "confidence", "x", "y", "width", "height"):
|
| 47 |
-
assert field in el, f"Missing field '{field}' in element"
|
| 48 |
-
|
| 49 |
-
def test_confidence_values_in_range(self, processor, text_drawing):
|
| 50 |
-
result = processor.extract_text(text_drawing)
|
| 51 |
-
for el in result["elements"]:
|
| 52 |
-
assert 0 <= el["confidence"] <= 100
|
| 53 |
-
|
| 54 |
-
def test_detects_dimension_number(self, processor, text_drawing):
|
| 55 |
-
result = processor.extract_text(text_drawing)
|
| 56 |
-
all_texts = [el["text"] for el in result["elements"]]
|
| 57 |
-
assert any("3640" in t for t in all_texts), \
|
| 58 |
-
f"Expected '3640' in OCR output, got: {all_texts}"
|
| 59 |
-
|
| 60 |
-
def test_classifies_bare_number_as_number(self, processor, text_drawing):
|
| 61 |
-
result = processor.extract_text(text_drawing)
|
| 62 |
-
num_texts = [el["text"] for el in result["classified"]["numbers"]]
|
| 63 |
-
assert "3640" in num_texts, \
|
| 64 |
-
f"'3640' (no unit) should be in numbers, got: {num_texts}"
|
| 65 |
-
|
| 66 |
-
def test_blank_image_returns_empty(self, processor):
|
| 67 |
-
blank = np.ones((400, 600), dtype=np.uint8) * 255
|
| 68 |
-
result = processor.extract_text(blank)
|
| 69 |
-
assert result["element_count"] == 0
|
| 70 |
-
assert result["elements"] == []
|
| 71 |
-
|
| 72 |
-
def test_full_text_is_string(self, processor, synthetic_drawing):
|
| 73 |
-
result = processor.extract_text(synthetic_drawing)
|
| 74 |
-
assert isinstance(result["full_text"], str)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
class TestClassify:
|
| 78 |
-
def test_dimension_pattern(self, processor):
|
| 79 |
-
elements = [{"text": "3640mm", "confidence": 90, "x": 0, "y": 0, "width": 50, "height": 20}]
|
| 80 |
-
classified = processor._classify(elements)
|
| 81 |
-
assert any(e["text"] == "3640mm" for e in classified["dimensions"])
|
| 82 |
-
|
| 83 |
-
def test_room_name_pattern(self, processor):
|
| 84 |
-
elements = [{"text": "Kitchen", "confidence": 90, "x": 0, "y": 0, "width": 80, "height": 20}]
|
| 85 |
-
classified = processor._classify(elements)
|
| 86 |
-
assert any(e["text"] == "Kitchen" for e in classified["room_names"])
|
| 87 |
-
|
| 88 |
-
def test_number_pattern(self, processor):
|
| 89 |
-
elements = [{"text": "42", "confidence": 90, "x": 0, "y": 0, "width": 30, "height": 20}]
|
| 90 |
-
classified = processor._classify(elements)
|
| 91 |
-
assert any(e["text"] == "42" for e in classified["numbers"])
|
| 92 |
-
|
| 93 |
-
def test_dimension_with_unit(self, processor):
|
| 94 |
-
elements = [{"text": "3640mm", "confidence": 90, "x": 0, "y": 0, "width": 60, "height": 20}]
|
| 95 |
-
classified = processor._classify(elements)
|
| 96 |
-
assert any(e["text"] == "3640mm" for e in classified["dimensions"])
|
| 97 |
-
|
| 98 |
-
def test_bare_number_not_in_dimensions(self, processor):
|
| 99 |
-
elements = [{"text": "100", "confidence": 90, "x": 0, "y": 0, "width": 40, "height": 20}]
|
| 100 |
-
classified = processor._classify(elements)
|
| 101 |
-
assert not any(e["text"] == "100" for e in classified["dimensions"])
|
| 102 |
-
|
| 103 |
-
def test_label_catchall(self, processor):
|
| 104 |
-
elements = [{"text": "note", "confidence": 90, "x": 0, "y": 0, "width": 40, "height": 20}]
|
| 105 |
-
classified = processor._classify(elements)
|
| 106 |
-
assert any(e["text"] == "note" for e in classified["labels"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_svg_generator.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unit tests for SVGGenerator.
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import pytest
|
| 6 |
-
|
| 7 |
-
from processors.svg_generator import SVGGenerator
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
@pytest.fixture
|
| 11 |
-
def generator():
|
| 12 |
-
return SVGGenerator()
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@pytest.fixture
|
| 16 |
-
def sample_lines():
|
| 17 |
-
return [
|
| 18 |
-
{"x1": 10, "y1": 10, "x2": 200, "y2": 10, "angle": 0.0, "length": 190.0},
|
| 19 |
-
{"x1": 10, "y1": 10, "x2": 10, "y2": 200, "angle": 90.0, "length": 190.0},
|
| 20 |
-
{"x1": 10, "y1": 10, "x2": 200, "y2": 200, "angle": 45.0, "length": 268.7},
|
| 21 |
-
]
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
@pytest.fixture
|
| 25 |
-
def sample_ocr():
|
| 26 |
-
return {
|
| 27 |
-
"elements": [
|
| 28 |
-
{"text": "Living Room", "confidence": 85, "x": 50, "y": 100, "width": 120, "height": 20},
|
| 29 |
-
{"text": "3640", "confidence": 90, "x": 50, "y": 150, "width": 60, "height": 20},
|
| 30 |
-
]
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
class TestGenerate:
|
| 35 |
-
def test_returns_string(self, generator, sample_lines):
|
| 36 |
-
result = generator.generate(sample_lines, 800, 600)
|
| 37 |
-
assert isinstance(result, str)
|
| 38 |
-
|
| 39 |
-
def test_output_is_valid_svg(self, generator, sample_lines):
|
| 40 |
-
result = generator.generate(sample_lines, 800, 600)
|
| 41 |
-
assert result.strip().startswith("<svg") or "<svg" in result
|
| 42 |
-
|
| 43 |
-
def test_svg_contains_correct_dimensions(self, generator, sample_lines):
|
| 44 |
-
result = generator.generate(sample_lines, 800, 600)
|
| 45 |
-
assert "800" in result
|
| 46 |
-
assert "600" in result
|
| 47 |
-
|
| 48 |
-
def test_svg_contains_line_elements(self, generator, sample_lines):
|
| 49 |
-
result = generator.generate(sample_lines, 800, 600)
|
| 50 |
-
assert "<line" in result
|
| 51 |
-
|
| 52 |
-
def test_wall_lines_group_present(self, generator, sample_lines):
|
| 53 |
-
result = generator.generate(sample_lines, 800, 600)
|
| 54 |
-
assert "wall-lines" in result
|
| 55 |
-
|
| 56 |
-
def test_detail_lines_group_present(self, generator, sample_lines):
|
| 57 |
-
result = generator.generate(sample_lines, 800, 600)
|
| 58 |
-
assert "detail-lines" in result
|
| 59 |
-
|
| 60 |
-
def test_svg_includes_ocr_text(self, generator, sample_lines, sample_ocr):
|
| 61 |
-
result = generator.generate(sample_lines, 800, 600, ocr_results=sample_ocr)
|
| 62 |
-
assert "Living Room" in result
|
| 63 |
-
assert "text-labels" in result
|
| 64 |
-
|
| 65 |
-
def test_svg_without_ocr_has_no_text_labels(self, generator, sample_lines):
|
| 66 |
-
result = generator.generate(sample_lines, 800, 600, ocr_results=None)
|
| 67 |
-
assert "text-labels" not in result
|
| 68 |
-
|
| 69 |
-
def test_empty_lines_produces_svg(self, generator):
|
| 70 |
-
result = generator.generate([], 400, 300)
|
| 71 |
-
assert "<svg" in result
|
| 72 |
-
|
| 73 |
-
def test_white_background_rect_present(self, generator, sample_lines):
|
| 74 |
-
result = generator.generate(sample_lines, 800, 600)
|
| 75 |
-
assert 'fill="white"' in result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|