AlihanSDev's picture
Add app.py
a7d9dbe verified
Raw
History Blame Contribute Delete
20 kB
"""
Digital Inspector β€” Gradio Space
Runs on CPU (no GPU required for YOLOv8n inference)
"""
import gradio as gr
import numpy as np
from PIL import Image, ImageDraw
import pandas as pd
from huggingface_hub import hf_hub_download
from ultralytics import YOLO
# ══════════════════════════════════════════════════════
# ΠšΠžΠΠ€Π˜Π“Π£Π ΠΠ¦Π˜Π―
# ══════════════════════════════════════════════════════
MODEL_REPO = "AlihanSDev/digital-inspector"
MODELS = {
"⭐ y8n_1024 β€” Best (fast + accurate, mAP=0.669)": ("y8n_1024", 1024),
"y8s_1024 β€” High precision (precision=0.985)": ("y8s_1024", 1024),
"y8m_1024 β€” Medium (mAP=0.649)": ("y8m_1024", 1024),
"y8l_1024 β€” Large (mAP=0.642)": ("y8l_1024", 1024),
"y8s_640 β€” Fast, low resolution": ("y8s_640", 640),
"y8s_768 β€” Medium resolution": ("y8s_768", 768),
}
CLASS_NAMES_RU = {0: "подпись", 1: "ΡˆΡ‚Π°ΠΌΠΏ", 2: "qr-ΠΊΠΎΠ΄"}
CLASS_NAMES_EN = {0: "signature", 1: "stamp", 2: "qr-code"}
CLASS_COLORS_RGB = {
0: (255, 80, 80),
1: (80, 180, 255),
2: (80, 255, 130),
}
# ══════════════════════════════════════════════════════
# ΠŸΠ•Π Π•Π’ΠžΠ”Π«
# ══════════════════════════════════════════════════════
T = {
"RU": {
"title": "# πŸ” Digital Inspector",
"subtitle": (
"**АвтоматичСскоС ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½ΠΈΠ΅ подписСй, ΡˆΡ‚Π°ΠΌΠΏΠΎΠ² ΠΈ QR-ΠΊΠΎΠ΄ΠΎΠ² "
"Π² Π΄Π΅Π»ΠΎΠ²Ρ‹Ρ… Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ…**\n\n"
"ΠžΠ±ΡƒΡ‡Π΅Π½ΠΎ Π½Π° русско- ΠΈ казахскоязычных Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ…. \n"
"Код: [GitHub](https://github.com/AlihanSDev/digital-inspector) | "
"МодСли: [HuggingFace](https://huggingface.co/AlihanSDev/digital-inspector)"
),
"model_label": "πŸ€– МодСль",
"conf_label": "ΠŸΠΎΡ€ΠΎΠ³ увСрСнности",
"iou_label": "ΠŸΠΎΡ€ΠΎΠ³ IOU (NMS)",
"lang_btn": "πŸ‡¬πŸ‡§ English",
"tab_image": "πŸ–ΌοΈ Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ (JPG / PNG)",
"tab_pdf": "πŸ“„ PDF Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚",
"img_input_label": "Π—Π°Π³Ρ€ΡƒΠ·ΠΈ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚",
"img_output_label": "Π Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚",
"img_btn": "πŸ” Π—Π°ΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ Π΄Π΅Ρ‚Π΅ΠΊΡ†ΠΈΡŽ",
"pdf_input_label": "Π—Π°Π³Ρ€ΡƒΠ·ΠΈ PDF",
"page_label": "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Π°",
"pdf_output_label": "Π Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚",
"pdf_btn": "πŸ” Π—Π°ΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ Π΄Π΅Ρ‚Π΅ΠΊΡ†ΠΈΡŽ",
"summary_label": "Π˜Ρ‚ΠΎΠ³",
"table_label": "Π”Π΅Ρ‚Π΅ΠΊΡ†ΠΈΠΈ",
"no_image": "Π—Π°Π³Ρ€ΡƒΠ·ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅",
"no_pdf": "Π—Π°Π³Ρ€ΡƒΠ·ΠΈ PDF Ρ„Π°ΠΉΠ»",
"found": "βœ… НайдСно: ",
"not_found": "⚠️ ΠžΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹. ΠŸΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉ ΡΠ½ΠΈΠ·ΠΈΡ‚ΡŒ ΠΏΠΎΡ€ΠΎΠ³ увСрСнности.",
"page_of": "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Π° {cur}/{total} β€” ",
"pdf_error": "❌ Ошибка чтСния PDF: ",
"metrics_title": "πŸ“Š ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ всСх ΠΌΠΎΠ΄Π΅Π»Π΅ΠΉ",
"about_title": "ℹ️ Об исслСдовании",
"metrics_md": """
## πŸ“Š Π Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Ρ‹ (Run 3 β€” консСрвативная аугмСнтация)
| МодСль | mAP@50 | mAP@50-95 | ms/img | ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ |
|--------|--------|-----------|--------|-----------|
| **y8n_1024** ⭐ | **0.881** | **0.669** | **237** | 3.2M |
| y8m_1024 | 0.872 | 0.649 | 308 | 25.9M |
| y8l_1024 | 0.858 | 0.642 | 316 | 43.7M |
| y8s_1024 | 0.831 | 0.630 | 249 | 11.2M |
| y8s_640 | 0.823 | 0.615 | 225 | 11.2M |
| y8s_768 | 0.836 | 0.596 | 233 | 11.2M |
**Π’Ρ‹Π²ΠΎΠ΄:** y8n (3.2M ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ²) ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΡ‚ y8l (43.7M) ΠΏΠΎ точности И скорости.
""",
"about_md": """
## О ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π΅
ΠŸΡ€ΠΎΠ²Π΅Π΄Π΅Π½ΠΎ **3 ΠΏΡ€ΠΎΠ³ΠΎΠ½Π°** обучСния:
| ΠŸΡ€ΠΎΠ³ΠΎΠ½ | АугмСнтация | mAP@50-95 |
|--------|-------------|-----------|
| Run 1 | Π‘Π΅Π· Π°ΡƒΠ³ΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ | 0.650 |
| Run 2 | Mosaic + copy-paste | 0.252 ❌ |
| **Run 3** | **ΠšΠΎΠ½ΡΠ΅Ρ€Π²Π°Ρ‚ΠΈΠ²Π½Π°Ρ** | **0.669** βœ… |
**Mosaic Π²Ρ€Π΅Π΄Π΅Π½ для Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ΠΎΠ²** β€” дСградация Π² 2.5Γ—.
### ΠšΠ»Π°ΡΡΡ‹
| Π¦Π²Π΅Ρ‚ | Класс | AP |
|------|-------|----|
| πŸ”΄ | Подпись (signature) | 0.355 |
| πŸ”΅ | Π¨Ρ‚Π°ΠΌΠΏ (stamp) | 0.982 |
| 🟒 | QR-ΠΊΠΎΠ΄ | β€” |
### ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΡ
- Π”Π°Π½Π½Ρ‹Π΅: Π΄Π΅Π»ΠΎΠ²Ρ‹Π΅ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Ρ‹ (рус/ΠΊΠ°Π·)
- ΠžΠΏΡ‚ΠΈΠΌΠ°Π»ΡŒΠ½ΠΎΠ΅ Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅: **1024px**
""",
},
"EN": {
"title": "# πŸ” Digital Inspector",
"subtitle": (
"**Automatic detection of signatures, stamps and QR codes "
"in business documents**\n\n"
"Trained on Russian and Kazakh language documents. \n"
"Code: [GitHub](https://github.com/AlihanSDev/digital-inspector) | "
"Models: [HuggingFace](https://huggingface.co/AlihanSDev/digital-inspector)"
),
"model_label": "πŸ€– Model",
"conf_label": "Confidence threshold",
"iou_label": "IOU threshold (NMS)",
"lang_btn": "πŸ‡·πŸ‡Ί Русский",
"tab_image": "πŸ–ΌοΈ Image (JPG / PNG)",
"tab_pdf": "πŸ“„ PDF document",
"img_input_label": "Upload document",
"img_output_label": "Result",
"img_btn": "πŸ” Run detection",
"pdf_input_label": "Upload PDF",
"page_label": "Page",
"pdf_output_label": "Result",
"pdf_btn": "πŸ” Run detection",
"summary_label": "Summary",
"table_label": "Detections",
"no_image": "Upload an image",
"no_pdf": "Upload a PDF file",
"found": "βœ… Found: ",
"not_found": "⚠️ Nothing found. Try lowering the confidence threshold.",
"page_of": "Page {cur}/{total} β€” ",
"pdf_error": "❌ PDF read error: ",
"metrics_title": "πŸ“Š All model metrics",
"about_title": "ℹ️ About the research",
"metrics_md": """
## πŸ“Š Results (Run 3 β€” Conservative Augmentation)
| Model | mAP@50 | mAP@50-95 | ms/img | Params |
|-------|--------|-----------|--------|--------|
| **y8n_1024** ⭐ | **0.881** | **0.669** | **237** | 3.2M |
| y8m_1024 | 0.872 | 0.649 | 308 | 25.9M |
| y8l_1024 | 0.858 | 0.642 | 316 | 43.7M |
| y8s_1024 | 0.831 | 0.630 | 249 | 11.2M |
| y8s_640 | 0.823 | 0.615 | 225 | 11.2M |
| y8s_768 | 0.836 | 0.596 | 233 | 11.2M |
**Key finding:** y8n (3.2M params) beats y8l (43.7M) in both accuracy AND speed.
""",
"about_md": """
## About the project
**3 training runs** with different augmentation strategies:
| Run | Augmentation | mAP@50-95 |
|-----|-------------|-----------|
| Run 1 | No augmentation | 0.650 |
| Run 2 | Mosaic + copy-paste | 0.252 ❌ |
| **Run 3** | **Conservative** | **0.669** βœ… |
**Mosaic augmentation is harmful for documents** β€” 2.5Γ— degradation.
### Classes
| Color | Class | AP |
|-------|-------|----|
| πŸ”΄ | Signature | 0.355 |
| πŸ”΅ | Stamp | 0.982 |
| 🟒 | QR code | β€” |
### Limitations
- Data: Russian/Kazakh business documents
- Optimal input resolution: **1024px**
""",
},
}
# ══════════════════════════════════════════════════════
# Π—ΠΠ“Π Π£Π—ΠšΠ ΠœΠžΠ”Π•Π›Π•Π™ (кэш)
# ══════════════════════════════════════════════════════
_model_cache: dict = {}
def load_model(model_folder: str) -> YOLO:
if model_folder in _model_cache:
return _model_cache[model_folder]
print(f"πŸ“₯ Loading {model_folder}...")
pt_path = hf_hub_download(
repo_id=MODEL_REPO,
filename=f"models/{model_folder}/best.pt",
)
# CPU inference β€” достаточно быстро для YOLOv8n
model = YOLO(pt_path)
_model_cache[model_folder] = model
print(f"βœ… {model_folder} ready!")
return model
# ══════════════════════════════════════════════════════
# Π˜ΠΠ€Π•Π Π•ΠΠ‘ (CPU)
# ══════════════════════════════════════════════════════
def run_detection(pil_image: Image.Image,
model_folder: str,
imgsz: int,
conf: float,
iou: float,
lang: str):
model = load_model(model_folder)
# device="" β†’ автоматичСски CPU Π½Π° HF Spaces
results = model.predict(
pil_image,
imgsz=imgsz,
conf=conf,
iou=iou,
device="cpu",
verbose=False,
)
img_out = pil_image.copy().convert("RGB")
draw = ImageDraw.Draw(img_out)
class_names = CLASS_NAMES_RU if lang == "RU" else CLASS_NAMES_EN
detections = []
boxes = results[0].boxes
if boxes is not None and len(boxes) > 0:
for box in boxes:
cls_id = int(box.cls[0])
conf_v = float(box.conf[0])
x1, y1, x2, y2 = [int(v) for v in box.xyxy[0]]
color = CLASS_COLORS_RGB.get(cls_id, (200, 200, 200))
cls_name = class_names.get(cls_id, f"class_{cls_id}")
draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
label = f"{cls_name} {conf_v:.2f}"
lbl_w = len(label) * 8 + 4
lbl_h = 22
draw.rectangle(
[x1, max(0, y1 - lbl_h), x1 + lbl_w, y1], fill=color)
draw.text(
(x1 + 2, max(0, y1 - lbl_h + 2)), label, fill=(0, 0, 0))
detections.append({
"class" if lang == "EN" else "класс": cls_name,
"conf": f"{conf_v:.3f}",
"X1": x1, "Y1": y1, "X2": x2, "Y2": y2,
"W" if lang == "EN" else "Π¨": x2 - x1,
"H" if lang == "EN" else "Π’": y2 - y1,
})
t = T[lang]
if detections:
counts: dict = {}
for d in detections:
key = d.get("class") or d.get("класс")
counts[key] = counts.get(key, 0) + 1
summary = t["found"] + ", ".join(f"{v} {k}" for k, v in counts.items())
else:
summary = t["not_found"]
return np.array(img_out), pd.DataFrame(detections), summary
# ══════════════════════════════════════════════════════
# ΠžΠ‘Π ΠΠ‘ΠžΠ’Π§Π˜ΠšΠ˜
# ══════════════════════════════════════════════════════
def handle_image(image_np, model_key, conf, iou, lang):
t = T[lang]
if image_np is None:
return None, pd.DataFrame(), t["no_image"]
model_folder, imgsz = MODELS[model_key]
pil_img = Image.fromarray(image_np).convert("RGB")
return run_detection(pil_img, model_folder, imgsz, conf, iou, lang)
def handle_pdf(pdf_file, model_key, conf, iou, page_num, lang):
t = T[lang]
if pdf_file is None:
return None, pd.DataFrame(), t["no_pdf"]
try:
from pdf2image import convert_from_path
pages = convert_from_path(pdf_file.name, dpi=200)
except Exception as e:
return None, pd.DataFrame(), t["pdf_error"] + str(e)
total = len(pages)
page_num = max(1, min(int(page_num), total))
pil_img = pages[page_num - 1]
model_folder, imgsz = MODELS[model_key]
img_out, df, summary = run_detection(
pil_img, model_folder, imgsz, conf, iou, lang)
summary = t["page_of"].format(cur=page_num, total=total) + summary
return img_out, df, summary
def handle_pdf_pages(pdf_file):
if pdf_file is None:
return gr.update(maximum=1, value=1)
try:
from pdf2image import convert_from_path
pages = convert_from_path(pdf_file.name, dpi=72)
return gr.update(maximum=len(pages), value=1)
except Exception:
return gr.update(maximum=1, value=1)
def switch_language(current_lang: str):
new_lang = "EN" if current_lang == "RU" else "RU"
t = T[new_lang]
return (
new_lang,
gr.update(value=t["title"]),
gr.update(value=t["subtitle"]),
gr.update(label=t["model_label"]),
gr.update(label=t["conf_label"]),
gr.update(label=t["iou_label"]),
gr.update(value=t["lang_btn"]),
gr.update(label=t["img_input_label"]),
gr.update(label=t["img_output_label"]),
gr.update(value=t["img_btn"]),
gr.update(label=t["pdf_input_label"]),
gr.update(label=t["page_label"]),
gr.update(label=t["pdf_output_label"]),
gr.update(value=t["pdf_btn"]),
gr.update(label=t["summary_label"]),
gr.update(label=t["table_label"]),
gr.update(label=t["summary_label"]),
gr.update(label=t["table_label"]),
gr.update(value=t["metrics_md"]),
gr.update(value=t["about_md"]),
gr.update(label=t["metrics_title"]),
gr.update(label=t["about_title"]),
)
# ══════════════════════════════════════════════════════
# GRADIO UI
# ══════════════════════════════════════════════════════
_t0 = T["RU"]
with gr.Blocks(title="Digital Inspector", theme=gr.themes.Soft()) as demo:
lang_state = gr.State("RU")
# ── Π¨Π°ΠΏΠΊΠ° ────────────────────────────
with gr.Row():
with gr.Column(scale=9):
title_md = gr.Markdown(_t0["title"])
subtitle_md = gr.Markdown(_t0["subtitle"])
with gr.Column(scale=1, min_width=130):
lang_btn = gr.Button(_t0["lang_btn"], variant="secondary", size="sm")
# ── ПанСль управлСния ─────────────────
with gr.Row():
model_dropdown = gr.Dropdown(
choices=list(MODELS.keys()),
value=list(MODELS.keys())[0],
label=_t0["model_label"],
scale=3,
)
conf_slider = gr.Slider(
minimum=0.05, maximum=0.95, value=0.25, step=0.05,
label=_t0["conf_label"], scale=2,
)
iou_slider = gr.Slider(
minimum=0.1, maximum=0.95, value=0.45, step=0.05,
label=_t0["iou_label"], scale=2,
)
# ── Π’ΠΊΠ»Π°Π΄ΠΊΠΈ ──────────────────────────
with gr.Tabs():
# Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅
with gr.TabItem(_t0["tab_image"]):
with gr.Row():
with gr.Column(scale=1):
img_input = gr.Image(
label=_t0["img_input_label"],
type="numpy", height=500)
img_btn = gr.Button(
_t0["img_btn"], variant="primary", size="lg")
with gr.Column(scale=1):
img_output = gr.Image(
label=_t0["img_output_label"],
type="numpy", height=500)
img_summary = gr.Textbox(
label=_t0["summary_label"], interactive=False)
img_table = gr.Dataframe(
label=_t0["table_label"], interactive=False)
# Волько click β€” ΡƒΠ±ΠΈΡ€Π°Π΅ΠΌ Π΄ΡƒΠ±Π»ΠΈΡ€ΡƒΡŽΡ‰ΠΈΠΉ change
img_btn.click(
fn=handle_image,
inputs=[img_input, model_dropdown,
conf_slider, iou_slider, lang_state],
outputs=[img_output, img_table, img_summary],
api_name="detect_image",
)
# PDF
with gr.TabItem(_t0["tab_pdf"]):
with gr.Row():
with gr.Column(scale=1):
pdf_input = gr.File(
label=_t0["pdf_input_label"],
file_types=[".pdf"])
page_slider = gr.Slider(
minimum=1, maximum=1, value=1, step=1,
label=_t0["page_label"])
pdf_btn = gr.Button(
_t0["pdf_btn"], variant="primary", size="lg")
with gr.Column(scale=1):
pdf_output = gr.Image(
label=_t0["pdf_output_label"],
type="numpy", height=500)
pdf_summary = gr.Textbox(
label=_t0["summary_label"], interactive=False)
pdf_table = gr.Dataframe(
label=_t0["table_label"], interactive=False)
pdf_input.change(
fn=handle_pdf_pages,
inputs=[pdf_input],
outputs=[page_slider],
api_name="update_pages",
)
pdf_btn.click(
fn=handle_pdf,
inputs=[pdf_input, model_dropdown,
conf_slider, iou_slider, page_slider, lang_state],
outputs=[pdf_output, pdf_table, pdf_summary],
api_name="detect_pdf",
)
# Π‘ΠΌΠ΅Π½Π° страницы β€” Ρ‚ΠΎΠΆΠ΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Π° функция
page_slider.change(
fn=handle_pdf,
inputs=[pdf_input, model_dropdown,
conf_slider, iou_slider, page_slider, lang_state],
outputs=[pdf_output, pdf_table, pdf_summary],
api_name="detect_pdf_page",
)
# ── АккордСоны ───────────────────────
with gr.Accordion(_t0["metrics_title"], open=False) as metrics_acc:
metrics_md = gr.Markdown(_t0["metrics_md"])
with gr.Accordion(_t0["about_title"], open=False) as about_acc:
about_md = gr.Markdown(_t0["about_md"])
# ── ΠŸΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ языка ────────────────
lang_btn.click(
fn=switch_language,
inputs=[lang_state],
outputs=[
lang_state,
title_md, subtitle_md,
model_dropdown, conf_slider, iou_slider,
lang_btn,
img_input, img_output, img_btn,
pdf_input, page_slider, pdf_output, pdf_btn,
img_summary, img_table,
pdf_summary, pdf_table,
metrics_md, about_md,
metrics_acc, about_acc,
],
api_name="switch_lang",
)
if __name__ == "__main__":
demo.launch()