| """Interactive Hugging Face Space for detection and segmentation demos.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import os |
|
|
| import gradio as gr |
|
|
| from vibe_ml_lab.detection_segmentation_lab import ( |
| FCN_FOCUS_CHOICES, |
| PROMPT_TEXT, |
| build_benchmark_sources_markdown, |
| build_delivery_markdown, |
| build_interface_mock, |
| generate_report_ui, |
| load_demo_image, |
| run_comparison_ui, |
| run_fcn_demo_ui, |
| run_mask_rcnn_demo_ui, |
| run_rcnn_family_ui, |
| ) |
|
|
| CUSTOM_CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Fraunces:opsz,wght@9..144,600;9..144,700&display=swap'); |
| |
| :root { |
| --paper: #f7f1e3; |
| --paper-2: #fffaf1; |
| --ink: #182333; |
| --muted: #536173; |
| --line: rgba(24, 35, 51, 0.12); |
| --coral: #f26b5b; |
| --sun: #f5b94c; |
| --mint: #44b89d; |
| --sky: #4e9cd3; |
| --berry: #c74c76; |
| --navy: #173047; |
| --shadow: 0 22px 70px rgba(24, 35, 51, 0.12); |
| } |
| |
| body, .gradio-container { |
| background: |
| radial-gradient(circle at 8% 12%, rgba(242, 107, 91, 0.16), transparent 18%), |
| radial-gradient(circle at 86% 10%, rgba(78, 156, 211, 0.14), transparent 22%), |
| radial-gradient(circle at 80% 78%, rgba(68, 184, 157, 0.12), transparent 20%), |
| linear-gradient(180deg, #efe7d7 0%, #f7f1e3 45%, #fffaf1 100%); |
| color: var(--ink); |
| font-family: 'Space Grotesk', 'Segoe UI', sans-serif !important; |
| } |
| |
| .gradio-container { max-width: 1440px !important; } |
| .app-shell { |
| border: 1px solid rgba(24, 35, 51, 0.08); |
| border-radius: 36px; |
| padding: 30px; |
| background: |
| linear-gradient(135deg, rgba(255,255,255,0.90), rgba(255,249,239,0.84)), |
| linear-gradient(180deg, rgba(255,255,255,0.40), rgba(255,255,255,0.08)); |
| box-shadow: var(--shadow); |
| backdrop-filter: blur(6px); |
| margin-bottom: 18px; |
| } |
| .hero-layout { |
| display: grid; |
| grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.9fr); |
| gap: 18px; |
| align-items: stretch; |
| } |
| .hero-main { |
| padding: 6px 4px 6px 4px; |
| } |
| .eyebrow { |
| display: inline-flex; |
| gap: 10px; |
| align-items: center; |
| border-radius: 999px; |
| padding: 8px 14px; |
| border: 1px solid rgba(24, 35, 51, 0.08); |
| background: rgba(255, 255, 255, 0.72); |
| font-size: 0.82rem; |
| text-transform: uppercase; |
| letter-spacing: 0.14em; |
| font-weight: 700; |
| } |
| .hero-title { |
| font-family: 'Fraunces', Georgia, serif; |
| font-size: clamp(2.6rem, 4.4vw, 4.4rem); |
| line-height: 0.92; |
| margin: 14px 0 16px 0; |
| max-width: 760px; |
| } |
| .hero-copy { |
| max-width: 760px; |
| color: var(--muted); |
| font-size: 1.05rem; |
| line-height: 1.82; |
| } |
| .hero-badges { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 10px; |
| margin-top: 20px; |
| } |
| .badge-chip { |
| display: inline-flex; |
| align-items: center; |
| gap: 8px; |
| padding: 10px 14px; |
| border-radius: 999px; |
| border: 1px solid rgba(24, 35, 51, 0.08); |
| background: rgba(255, 255, 255, 0.74); |
| color: var(--ink); |
| font-size: 0.92rem; |
| font-weight: 700; |
| } |
| .hero-side { |
| display: grid; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| gap: 14px; |
| } |
| .spot-card { |
| min-height: 138px; |
| border-radius: 26px; |
| padding: 18px; |
| color: #ffffff; |
| box-shadow: inset 0 1px 0 rgba(255,255,255,0.18); |
| } |
| .spot-card strong { |
| display: block; |
| font-family: 'Fraunces', Georgia, serif; |
| font-size: 1.28rem; |
| margin-bottom: 8px; |
| } |
| .spot-card p { |
| margin: 0; |
| line-height: 1.66; |
| font-size: 0.94rem; |
| } |
| .spot-card.coral { background: linear-gradient(145deg, #f26b5b, #db5948); } |
| .spot-card.sun { background: linear-gradient(145deg, #f5b94c, #d99429); color: #2b2b2b; } |
| .spot-card.sky { background: linear-gradient(145deg, #4e9cd3, #2d78ad); } |
| .spot-card.mint { background: linear-gradient(145deg, #44b89d, #2d8c76); } |
| .guide-shell { |
| border-radius: 30px; |
| padding: 24px; |
| border: 1px solid rgba(24, 35, 51, 0.08); |
| background: linear-gradient(180deg, rgba(255,255,255,0.90), rgba(250,244,232,0.94)); |
| box-shadow: 0 16px 40px rgba(24, 35, 51, 0.06); |
| } |
| .guide-title { |
| font-family: 'Fraunces', Georgia, serif; |
| font-size: 2rem; |
| margin-bottom: 8px; |
| } |
| .guide-copy { |
| color: var(--muted); |
| line-height: 1.74; |
| margin-bottom: 18px; |
| } |
| .guide-grid { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| gap: 12px; |
| } |
| .guide-card { |
| border-radius: 22px; |
| padding: 16px; |
| background: rgba(255,255,255,0.82); |
| border: 1px solid rgba(24, 35, 51, 0.08); |
| } |
| .guide-card strong { |
| display: block; |
| font-size: 1rem; |
| margin-bottom: 6px; |
| } |
| .guide-card span { |
| color: var(--muted); |
| line-height: 1.6; |
| font-size: 0.92rem; |
| } |
| .note-card, .metric-card { |
| border-radius: 24px; |
| border: 1px solid rgba(24, 35, 51, 0.10); |
| background: rgba(255, 255, 255, 0.78); |
| box-shadow: 0 12px 28px rgba(24, 35, 51, 0.06); |
| } |
| .note-card { padding: 18px 20px; } |
| .metric-card { |
| padding: 12px 16px; |
| background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(249,243,232,0.88)); |
| } |
| .metric-card p { margin: 0; } |
| .tabs button { |
| font-weight: 700 !important; |
| border-radius: 999px !important; |
| } |
| button.primary { |
| background: linear-gradient(135deg, var(--coral), var(--sun)) !important; |
| border: none !important; |
| box-shadow: 0 8px 20px rgba(242, 107, 91, 0.25) !important; |
| } |
| .subtle { |
| color: var(--muted); |
| font-weight: 700; |
| } |
| .mock-wrap { |
| border-radius: 30px; |
| overflow: hidden; |
| border: 1px solid rgba(24, 35, 51, 0.08); |
| box-shadow: 0 18px 44px rgba(24, 35, 51, 0.08); |
| } |
| @media (max-width: 980px) { |
| .hero-layout { grid-template-columns: 1fr; } |
| .guide-grid, .hero-side { grid-template-columns: repeat(2, minmax(0, 1fr)); } |
| } |
| @media (max-width: 640px) { |
| .app-shell { padding: 22px; border-radius: 26px; } |
| .guide-grid, .hero-side { grid-template-columns: repeat(1, minmax(0, 1fr)); } |
| } |
| """ |
|
|
| APP_THEME = gr.themes.Base( |
| primary_hue="orange", |
| secondary_hue="cyan", |
| neutral_hue="stone", |
| radius_size="lg", |
| ) |
|
|
|
|
| def build_interface() -> gr.Blocks: |
| demo_image = load_demo_image() |
| with gr.Blocks(title="Detection + Segmentation Studio") as demo: |
| gr.HTML( |
| """ |
| <div class="app-shell"> |
| <div class="hero-layout"> |
| <div class="hero-main"> |
| <div class="eyebrow">Vision Lab x Hugging Face Space</div> |
| <div class="hero-title">上传一张图,直接对比 FCN、R-CNN 家族和 Mask R-CNN</div> |
| <div class="hero-copy"> |
| 这个页面把语义分割、目标检测、实例分割和综合性能对比收进同一个交互式实验站。 |
| 每个实验区都预载了示例图,你可以直接点,也可以现场换成自己的图片,看结果、看速度、看差异。 |
| </div> |
| <div class="hero-badges"> |
| <div class="badge-chip">上传图片 / 剪贴板 / 摄像头</div> |
| <div class="badge-chip">在线推理 + 可视化结果</div> |
| <div class="badge-chip">综合性能对比页</div> |
| <div class="badge-chip">PDF 实验报告导出</div> |
| </div> |
| </div> |
| <div class="hero-side"> |
| <div class="spot-card coral"><strong>FCN 语义分割</strong><p>整图像素级预测,适合看前景覆盖和语义区域叠加。</p></div> |
| <div class="spot-card sun"><strong>R-CNN 家族</strong><p>同一张图里对比 proposal、共享特征和 RPN 的差别。</p></div> |
| <div class="spot-card sky"><strong>Mask R-CNN</strong><p>除了框和类别,还能直接看到实例级 mask 结果。</p></div> |
| <div class="spot-card mint"><strong>性能总览</strong><p>把前面几块方法放到一张总览图里,同时比较速度和公开指标。</p></div> |
| </div> |
| </div> |
| </div> |
| """ |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(scale=3): |
| gr.HTML( |
| """ |
| <div class="guide-shell"> |
| <div class="guide-title">Quick Start</div> |
| <div class="guide-copy"> |
| 现在可以不用先研究页面结构了,直接从任意标签页开始。前面三个实验区负责“看结果”,最后一个实验区负责“把前面几种方法拉到一起比较”。 |
| </div> |
| <div class="guide-grid"> |
| <div class="guide-card"><strong>01 直接试跑</strong><span>每个标签页都预载了示例图,点击按钮就能出结果。</span></div> |
| <div class="guide-card"><strong>02 换你自己的图</strong><span>支持上传、剪贴板和摄像头输入,适合现场演示。</span></div> |
| <div class="guide-card"><strong>03 看综合比较</strong><span>在性能页同时比较五种方法的输出、公开指标和当前图片速度。</span></div> |
| </div> |
| </div> |
| """ |
| ) |
| with gr.Column(scale=2): |
| gr.Image( |
| value=build_interface_mock(), |
| label="界面预览", |
| type="numpy", |
| elem_classes=["mock-wrap"], |
| ) |
|
|
| with gr.Tabs(): |
| with gr.Tab("FCN 语义分割"): |
| gr.Markdown( |
| "适合看整图像素级预测、语义区域叠加,以及不同类别在当前图片中的覆盖比例。", |
| elem_classes=["note-card"], |
| ) |
| gr.Markdown("已预载示例图,可以直接点击运行;也可以换成你自己的图片。", elem_classes=["subtle"]) |
| with gr.Row(): |
| with gr.Column(scale=1): |
| fcn_input = gr.Image( |
| value=demo_image, |
| label="上传图片", |
| type="numpy", |
| sources=["upload", "clipboard", "webcam"], |
| ) |
| fcn_alpha = gr.Slider(0.15, 0.85, value=0.50, step=0.05, label="Overlay 透明度") |
| fcn_focus = gr.Dropdown(choices=FCN_FOCUS_CHOICES, value=FCN_FOCUS_CHOICES[0], label="重点查看类别") |
| fcn_ratio = gr.Slider(0.0, 0.15, value=0.01, step=0.01, label="最小像素占比过滤") |
| fcn_button = gr.Button("运行 FCN 分割", variant="primary") |
| with gr.Column(scale=2): |
| fcn_summary = gr.Markdown(elem_classes=["metric-card"]) |
| with gr.Row(): |
| fcn_overlay = gr.Image(label="分割叠加结果", type="numpy") |
| fcn_legend = gr.Image(label="类别图例与分布", type="numpy") |
| fcn_table = gr.Dataframe(headers=["Class", "Pixel count", "Ratio", "Highlighted"], wrap=True, interactive=False) |
| fcn_button.click( |
| run_fcn_demo_ui, |
| [fcn_input, fcn_alpha, fcn_focus, fcn_ratio], |
| [fcn_summary, fcn_overlay, fcn_legend, fcn_table], |
| ) |
|
|
| with gr.Tab("R-CNN / Fast / Faster R-CNN"): |
| gr.Markdown( |
| "这里把 `R-CNN`、`Fast R-CNN`、`Faster R-CNN` 放到同一个实验区里,方便你在同一张图上观察 proposal、共享特征和 RPN 的差别。", |
| elem_classes=["note-card"], |
| ) |
| gr.Markdown("已预载示例图,可以直接点击运行;也可以换成你自己的图片。", elem_classes=["subtle"]) |
| with gr.Row(): |
| with gr.Column(scale=1): |
| det_input = gr.Image( |
| value=demo_image, |
| label="上传图片", |
| type="numpy", |
| sources=["upload", "clipboard", "webcam"], |
| ) |
| det_method = gr.Radio(["R-CNN", "Fast R-CNN", "Faster R-CNN"], value="Faster R-CNN", label="选择方法") |
| det_proposals = gr.Slider(8, 40, value=18, step=2, label="候选区域数量") |
| det_threshold = gr.Slider(0.15, 0.85, value=0.45, step=0.05, label="检测分数阈值") |
| det_button = gr.Button("运行检测实验", variant="primary") |
| with gr.Column(scale=2): |
| det_summary = gr.Markdown(elem_classes=["metric-card"]) |
| with gr.Row(): |
| det_main = gr.Image(label="主结果图", type="numpy") |
| det_aux = gr.Image(label="流程 / 候选框辅助图", type="numpy") |
| det_table = gr.Dataframe(headers=["Item", "Label", "Score", "IoU/ref", "Area ratio"], wrap=True, interactive=False) |
| det_button.click( |
| run_rcnn_family_ui, |
| [det_input, det_method, det_proposals, det_threshold], |
| [det_summary, det_main, det_aux, det_table], |
| ) |
|
|
| with gr.Tab("Mask R-CNN 实例分割"): |
| gr.Markdown( |
| "这个模块会直接给出实例级分割结果,展示每个实例的类别、分数、边框和 mask 轮廓。", |
| elem_classes=["note-card"], |
| ) |
| gr.Markdown("已预载示例图,可以直接点击运行;也可以换成你自己的图片。", elem_classes=["subtle"]) |
| with gr.Row(): |
| with gr.Column(scale=1): |
| mask_input = gr.Image( |
| value=demo_image, |
| label="上传图片", |
| type="numpy", |
| sources=["upload", "clipboard", "webcam"], |
| ) |
| mask_threshold = gr.Slider(0.20, 0.90, value=0.55, step=0.05, label="实例分数阈值") |
| mask_alpha = gr.Slider(0.15, 0.85, value=0.48, step=0.05, label="Mask 透明度") |
| mask_count = gr.Slider(1, 10, value=6, step=1, label="最多展示实例数") |
| mask_button = gr.Button("运行 Mask R-CNN", variant="primary") |
| with gr.Column(scale=2): |
| mask_summary = gr.Markdown(elem_classes=["metric-card"]) |
| with gr.Row(): |
| mask_overlay = gr.Image(label="实例分割叠加结果", type="numpy") |
| mask_gallery = gr.Image(label="实例裁剪与 mask 预览", type="numpy") |
| mask_table = gr.Dataframe(headers=["Instance", "Class", "Score", "Mask area", "BBox"], wrap=True, interactive=False) |
| mask_button.click( |
| run_mask_rcnn_demo_ui, |
| [mask_input, mask_threshold, mask_alpha, mask_count], |
| [mask_summary, mask_overlay, mask_gallery, mask_table], |
| ) |
|
|
| with gr.Tab("性能对比 + 实验报告"): |
| gr.Markdown( |
| "这一页会把前面几块方法放到同一张总览图里,同时比较公开指标、结构差异,以及当前图片下的速度表现。", |
| elem_classes=["note-card"], |
| ) |
| gr.Markdown("已预载示例图,可以直接点击生成性能对比;也可以上传你的图片重新测。", elem_classes=["subtle"]) |
| with gr.Row(): |
| with gr.Column(scale=1): |
| cmp_input = gr.Image( |
| value=demo_image, |
| label="上传图片(用于实时 benchmark,可选)", |
| type="numpy", |
| sources=["upload", "clipboard", "webcam"], |
| ) |
| cmp_threshold = gr.Slider(0.15, 0.85, value=0.45, step=0.05, label="统一阈值") |
| cmp_proposals = gr.Slider(8, 40, value=18, step=2, label="R-CNN 模拟候选框数量") |
| cmp_button = gr.Button("生成性能对比", variant="primary") |
| with gr.Column(scale=2): |
| cmp_summary = gr.Markdown(elem_classes=["metric-card"]) |
| with gr.Row(): |
| cmp_plot = gr.Image(label="五种方法结果总览", type="numpy") |
| cmp_runtime = gr.Image(label="公开指标 + 当前图片速度", type="numpy") |
| cmp_table = gr.Dataframe( |
| headers=["Method", "Task", "Current image output", "Published metric", "Live runtime", "Runtime type", "Structure clue"], |
| wrap=True, |
| interactive=False, |
| ) |
| cmp_button.click( |
| run_comparison_ui, |
| [cmp_input, cmp_threshold, cmp_proposals], |
| [cmp_summary, cmp_plot, cmp_runtime, cmp_table], |
| ) |
| gr.Markdown(build_benchmark_sources_markdown(), elem_classes=["note-card"]) |
|
|
| gr.Markdown("### Prompt 文本", elem_classes=["subtle"]) |
| gr.Textbox(value=PROMPT_TEXT, label="本次 Vibe Coding 设计 Prompt", lines=9, max_lines=12) |
| gr.Markdown(build_delivery_markdown(), elem_classes=["note-card"]) |
| with gr.Row(): |
| report_button = gr.Button("生成 PDF 实验报告", variant="primary") |
| report_summary = gr.Markdown(elem_classes=["metric-card"]) |
| report_file = gr.File(label="下载生成的 PDF / Prompt") |
| report_button.click(generate_report_ui, outputs=[report_summary, report_file]) |
| return demo |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description="Launch the Detection + Segmentation Studio app.") |
| parser.add_argument("--share", action="store_true", help="Create a temporary Gradio sharing link.") |
| parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "7860")), help="Server port.") |
| parser.add_argument("--no-browser", action="store_true", help="Do not open a browser window.") |
| args = parser.parse_args() |
|
|
| demo = build_interface() |
| demo.queue(default_concurrency_limit=2) |
| demo.launch( |
| server_name=os.environ.get("HOST", "0.0.0.0"), |
| server_port=args.port, |
| share=args.share, |
| inbrowser=not args.no_browser, |
| css=CUSTOM_CSS, |
| theme=APP_THEME, |
| show_error=True, |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|